Hi, I'm who's behind Fedify, Hollo, BotKit, and this website, Hackers' Pub!

Fedify, Hollo, BotKit, 그리고 보고 계신 이 사이트 Hackers' Pub을 만들고 있습니다.

FedifyHolloBotKit、そしてこのサイト、Hackers' Pubを作っています。

嗨,我是 FedifyHolloBotKit 以及這個網站 Hackers' Pub 的開發者!

Website
hongminhee.org
GitHub
@dahlia
Hollo
@hongminhee@hollo.social
DEV
@hongminhee
velog
@hongminhee
Qiita
@hongminhee
Zenn
@hongminhee
Matrix
@hongminhee:matrix.org
X
@hongminhee

한국 소프트웨어 개발자들이 자주 틀리는 외래어 표기법

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

전에 단문으로 올렸던 글을 지속적으로 갱신해볼까 싶어 게시글로 만들어 봅니다.

영어 틀린 표기 올바른 표기
algorithm 알고리 알고리
app 어플
application 플리케이션 플리케이션
BASIC 베이 베이
directory 디렉 디렉
front-end 트엔드 트엔드
launch
license 라이 라이
message
method
parallel 페러 패럴
proxy
release 릴리 릴리
repository 레포 리파
shader 이더 이더
shell
Read more →
0

Browser-Native Translation and Language Detection APIs Coming Soon

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

Just reviewed the W3C draft for the Translator and Language Detector APIs. This is genuinely exciting development for web developers.

The proposal would add native browser support for:

  • Text translation between languages
  • Language detection of arbitrary text
  • Both with streaming capabilities

No more relying on third-party translation services or embedding external APIs for basic language operations. All processing happens locally in the browser.

The API design is clean and straightforward:

// Translation example
const translator = await Translator.create({
  sourceLanguage: "en",
  targetLanguage: "fr"
});

const translatedText = await translator.translate("Hello world");

// Language detection example
const detector = await LanguageDetector.create();
const results = await detector.detect("Hello world");
// Returns array of detected languages with confidence scores

This will be a game-changer for multilingual sites and applications. The browser handles downloading appropriate language models and manages usage quotas.

The spec is still in draft form but shows promising progress toward standardizing these capabilities across browsers. Looking forward to seeing this implemented.

Read more →
1

Bluesky는 X의 훌륭한 대안일 수 있지만, 연합우주의 대안은 아닙니다

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

최근 X(구 Twitter)를 떠나는 사람들이 늘면서 Bluesky에 대한 관심이 뜨겁습니다. Bluesky는 깔끔한 인터페이스와 과거 Twitter와 유사한 사용자 경험을 제공하며, 신뢰할 수 있는 이탈(credible exit)이라는 매력적인 개념을 내세워 X의 유력한 대안으로 떠오르고 있습니다. 하지만 Bluesky와 그 기반 프로토콜인 AT Protocol을 연합우주(fediverse)의 대안으로 보기에는 근본적인 차이가 존재합니다. 이 글에서는 Christine Lemmer-Webber 씨(@cwebber)의 날카로운 분석(〈Bluesky는 실제로 얼마나 탈중앙화 되어 있나〉 및 〈답장: 답장: Bluesky와 탈중앙화〉)을 바탕으로, Bryan Newbold 씨(@bnewbold)의 반론(〈Bluesky와 탈중앙화에 대한 답변〉)을 충분히 고려하면서 Bluesky가 어째서 X의 대안은 될 수 있어도 연합우주의 대안은 될 수 없는지 이야기를 풀어볼까 합니다.

메시지 전달 對 공유 힙: 근본적인 설계 차이

Bluesky와 연합우주의 가장 큰 차이점 중 하나는 설계입니다. 연합우주는 이메일이나 XMPP와 유사한 메시지 전달(message passing) 방식을 채택하고 있습니다. 이는 특정 수신자에게 메시지를 직접 전달하는 방식으로, 효율성이 높습니다. 예를 들어, 수많은 서버 중 단 몇 곳의 사용자만 특정 메시지에 관심을 있다면 해당 서버에만 메시지를 전달하면 됩니다. 비유하자면, 철수가 영희에게 편지를 보내려면 직접 영희의 집으로 편지를 보내고, 영희가 회신하고 싶으면 직접 철수에게 회신하는 것과 같은 방식입니다.

반면, Bluesky는 공유 힙(shared heap) 방식을 사용합니다. 이는 메시지를 특정 수신자에게 직접 보내는 대신, 모든 메시지를 중앙의 “릴레이”라는 곳에 저장하고, 관심 있는 사용자가 릴레이에서 자신에게 필요한 정보를 필터링하는 방식입니다. 이는 마치 모든 편지가 하나의 거대한 우체국(릴레이)에 쌓이고, 각자가 이 우체국에 방문하여 자신에게 관련된 편지를 직접 찾아야 하는 것과 같습니다. 이런 방식에서는 메시지가 직접 전달되지 않기 때문에, 답글이 어떤 메시지에 대한 것인지 파악하려면 모든 가능한 메시지를 알고 있어야 합니다.

이 설계는 데이터와 색인을 분리하여 유연성을 제공한다는 주장도 있지만, 필연적으로 대규모 중앙 집권화된 릴레이에 의존하게 되어 탈중앙화의 이상과는 거리가 멀어진다는 한계가 있습니다.

결국 Bluesky가 공유 힙 방식을 채택하고 중앙 집권화된 릴레이에 의존하게 되는 데에는 운영 비용이라는 현실적인 이유가 크게 작용합니다. Christine Lemmer-Webber 씨의 분석에 따르면, Bluesky에서 전체 네트워크 기록을 저장하는 릴레이를 운영하는 데에는 상당한 스토리지를 요구하며, 이는 빠르게 증가하고 있습니다. 2024년 7월에는 약 1TB의 저장 공간이 필요했지만, 불과 4개월 후인 11월에는 약 5TB로 증가했습니다. 상업용 호스팅 서비스 기준으로 이는 연간 수만 달러(약 $55,000)에 달하는 비용이 발생할 수 있습니다.

반면, 연합우주에서는 개인이나 소규모 단체가 Raspberry Pi와 같은 저렴한 장비로도 GoToSocial과 같은 소프트웨어를 실행하여 독립적인 노드를 운영할 수 있습니다. 물론 대규모 연합우주 인스턴스는 더 많은 비용이 들겠지만, Bluesky의 전체 릴레이 운영 비용과는 비교하기 어려울 정도로 저렴합니다. 이처럼 운영 비용의 현격한 차이는 Bluesky가 분산된 구조를 채택하기 어렵게 만들고, 결국 중앙 집권화된 릴레이에 의존하게 만드는 주요 원인이라고 볼 수 있습니다.

전역 뷰에 대한 집착과 중앙 집권화의 심화

Bluesky는 댓글 누락과 같은 문제를 피하기 위해 네트워크 전체의 일관된 전역 뷰를 유지하는 데 집중하는 것으로 보입니다. 이러한 목표는 사용자 경험 측면에서 긍정적일 수 있지만, 필연적으로 중앙 집권화를 야기합니다. 대표적인 예가 차단 목록의 전체 공개입니다. 네트워크 전체의 일관성을 유지하기 위해 누가 누구를 차단했는지 모든 앱뷰가 알아야 하므로, 차단 정보가 공개되는 것입니다.

이는 개인 정보 보호 측면에서 심각한 우려를 낳을 수 있습니다. 단순히 누군가의 게시물을 보고 차단된 사람을 추측하는 것과, 네트워크에 “J. K. Rowling[1]을 차단한 모든 사람”을 직접 질의할 수 있는 것 사이에는 큰 차이가 있습니다. 실제로 ActivityPub 개발 과정에서는 이런 문제를 고려하여 서버 간에 차단 활동을 전달하지 않도록 명시적으로 설계했습니다. 이는 차단한 사람이 차단당한 사람의 보복을 받을 위험을 줄이기 위함입니다.

반면 연합우주에서는 각 서버가 독립적으로 차단 정책을 시행하며, 사용자에게 더 많은 자율성을 제공합니다.

AT Protocol과 개방형 표준으로서의 ActivityPub

연합우주의 핵심 프로토콜인 ActivityPub은 W3C의 채택 권고안으로, 개방형 표준입니다. 이는 누구나 자유롭게 구현하고 사용할 수 있으며, 다양한 소프트웨어 간의 상호 운용성을 보장합니다. 현재 페디버스 커뮤니티는 FEP를 중심으로 활발하게 프로토콜을 개선하고 발전시켜 나가고 있습니다. 반면, Bluesky의 AT Protocol은 아직 특정 사기업에 의해 주도되고 있으며, 개방형 표준으로서의 지위는 아직 확립되지 않았습니다. 이는 페디버스가 가진 확장성과 지속 가능성 측면에서 중요한 차이점이라고 할 수 있습니다.

DM의 중앙화

Bluesky는 콘텐츠 주소 지정이나 이동 가능한 아이덴티티와 같은 탈중앙화 요소를 도입했지만, DM은 완전히 중앙화되어 있습니다. 사용자가 어떤 PDS를 사용하든, 어떤 릴레이를 사용하든 상관없이 모든 DM은 Bluesky 회사를 통해 전송됩니다.

이는 Bluesky가 아직 기능적으로 완전한 Twitter 대체품이 되기 위해 속도를 우선시했다는 증거입니다. Bluesky는 이 DM 시스템이 장기적인 솔루션이 아니라고 밝혔지만, 대부분의 사용자들은 이 사실을 인지하지 못하고 있으며 DM도 AT Protocol의 다른 기능처럼 작동한다고 가정합니다.

이러한 중앙화된 DM 구현은 “신뢰할 수 있는 이탈”이라는 Bluesky의 핵심 가치와도 모순됩니다. 만약 Bluesky社가 적대적인 인수나 정책 변경을 겪게 된다면, 사용자들의 개인 대화는 완전히 회사의 통제 하에 남게 됩니다.

이동 가능한 아이덴티티와 DID: Bluesky 방식의 한계

Bluesky는 이동 가능한 아이덴티티(portable identity)를 핵심적인 장점 중 하나로 내세우며, 이를 위해 DIDs, 즉 분산 식별자를 활용합니다. 이는 사용자가 자신의 계정과 데이터를 다른 플랫폼으로 쉽게 이동할 수 있도록 하는 중요한 기능입니다. 하지만 Christine Lemmer-Webber는 AT Protocol이 채택한 did:webdid:plc 방식이 여전히 DNS와 Bluesky社가 관리하는 중앙 집권화된 PLC 레지스트리에 의존하고 있어 완전한 사용자 통제하의 독립적인 아이덴티티를 제공하는지 의문을 제기합니다.

더 놀라운 점은 Bluesky社가 초기에 모든 계정에 대해 동일한 rotationKeys를 사용했다는 사실입니다. 이는 클라우드 HSM 제품이 키별로 비용을 청구해서 각 사용자에게 고유한 키를 제공하는 것이 금전적으로 비용이 많이 들었기 때문이라고 합니다. 이러한 접근 방식은 DIDs 시스템을 구축하는 근본적인 목표와 모순되는 것으로 보입니다.

중요한 점은 DIDs 기술 자체가 탈중앙화된 아이덴티티를 위한 잠재력을 가지고 있음에도, Bluesky와 AT Protocol이 채택한 특정 방식이 중앙 집권화된 요소에 의존한다는 것입니다. 블록체인 기반의 DIDs와 같은 진정으로 탈중앙화된 방식도 존재하지만, AT Protocol은 비교적 구현이 쉬운 did:webdid:plc를 선택했습니다. 따라서 사용자가 Bluesky 생태계를 벗어나 자신의 아이덴티티를 완전히 독립적으로 관리하고자 할 때 제약이 발생할 수 있습니다.

또한 현재 시스템에서는 Bluesky社가 사용자의 키를 대신 관리하고 있어, 사용자가 현재는 Bluesky社를 신뢰하더라도 미래에 신뢰하지 않게 된 경우에도 여전히 회사에 의존해야 합니다. Bluesky社가 사용자를 대신하여 이동을 수행하도록 신뢰해야 하며, 심지어 Bluesky社가 사용자에게 향후 신원 정보를 제어할 권한을 위임하더라도 Bluesky社는 항상 해당 사용자의 키를 통제할 것입니다.

한편, 연합우주에서는 이미 노마딕 아이덴티티(nomadic identity)라는 개념을 통해 이동 가능한 아이덴티티에 대한 논의와 연구가 활발하게 진행되어 왔습니다. 이는 단순히 계정을 이전하는 것을 넘어, 사용자의 데이터와 관계, 심지어 평판까지도 자유롭게 이동할 수 있도록 하는 더 포괄적인 개념입니다. 《We Distribute》에 실린 기사 〈오, Zot! ActivityPub에 노마딕 아이덴티티가 도입된다〉에 소개된 Zot 프로토콜과 같은 기술은 이미 연합우주 안에서 이러한 노마딕 아이덴티티를 구현하기 위한 메커니즘을 제공하고 있습니다. 또한, FEP-ef61와 같은 제안을 통해 ActivityPub 자체를 개선하여 더 나은 이동 가능한 아이덴티티 기능을 추가하려는 노력도 진행 중입니다.

그래서, 결론은?

결론적으로, Bluesky는 사용자 친화적인 인터페이스와 신뢰할 수 있는 이탈 기능을 통해 X의 훌륭한 대안이 될 수 있습니다. Bluesky는 콘텐츠 주소 지정 방식을 통해 노드가 다운되더라도 콘텐츠가 살아남을 수 있게 하는 등 연합우주가 아직 충분히 활용하지 못하는 몇 가지 강점도 가지고 있습니다.

하지만 중앙 집권화된 설계, 전역 뷰에 대한 집착으로 인한 부작용, 개방형 표준으로서의 한계, DM의 중앙화, 그리고 이동 가능한 아이덴티티 구현의 제한점 등 여러 측면에서 연합우주의 대안으로 보기는 어렵습니다. 연합우주는 메시지 전달 방식의 분산된 아키텍처, 낮은 참여 장벽, 개방형 표준 기반의 활발한 커뮤니티 개발, 그리고 사용자에게 더 많은 자율성과 통제권을 제공하는 철학을 바탕으로 구축된, 근본적으로 다른 종류의 탈중앙화 소셜 네트워크입니다.

또한, Bluesky社가 벤처 캐피털 자금을 확보함에 따라 “조직은 미래의 적이다”라는 그들의 자체 인식에도 불구하고, 투자자 수익과 플랫폼 성장이라는 상업적 압력이 진정한 탈중앙화 추구보다 우선시될 위험이 있습니다. 특히 유료 계정과 광고가 도입되면서 이러한 우려는 더욱 커질 수 있습니다.

따라서 Bluesky는 X를 대체할 수 있을지 모르지만, 연합우주가 제공하는 탈중앙화된 가치와 경험을 대체하기는 어려울 것이라고 생각합니다. 두 시스템은 근본적으로 다른 목표와 설계 철학을 가지고 있으며, 이상적으로는 서로를 보완하는 방향으로 발전해 나갈 수 있을 것입니다.


  1. 판타지 소설 시리즈 《해리 포터》의 작가. ↩︎

Read more →
0

Revisiting Java's Checked Exceptions: An Underappreciated Type Safety Feature

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

Despite their bad reputation in the Java community, checked exceptions provide superior type safety comparable to Rust's Result<T, E> or Haskell's Either a b—we've been dismissing one of Java's best features all along.

Introduction

Few features in Java have been as consistently criticized as checked exceptions. Modern Java libraries and frameworks often go to great lengths to avoid them. Newer JVM languages like Kotlin have abandoned them entirely. Many experienced Java developers consider them a design mistake.

But what if this conventional wisdom is wrong? What if checked exceptions represent one of Java's most forward-thinking features?

In this post, I'll argue that Java's checked exceptions were ahead of their time, offering many of the same type safety benefits that are now celebrated in languages like Rust and Haskell. Rather than abandoning this feature, we should consider how to improve it to work better with modern Java's features.

Understanding Java's Exception Handling Model

To set the stage, let's review how Java's exception system works:

  • Unchecked exceptions (subclasses of RuntimeException or Error): These don't need to be declared or caught. They typically represent programming errors (NullPointerException, IndexOutOfBoundsException) or unrecoverable conditions (OutOfMemoryError).

  • Checked exceptions (subclasses of Exception but not RuntimeException): These must either be caught with try/catch blocks or declared in the method signature with throws. They represent recoverable conditions that are outside the normal flow of execution (IOException, SQLException).

Here's how this works in practice:

// Checked exception - compiler forces you to handle or declare it
public void readFile(String path) throws IOException {
    Files.readAllLines(Path.of(path));
}

// Unchecked exception - no compiler enforcement
public void processArray(int[] array) {
    int value = array[array.length + 1]; // May throw ArrayIndexOutOfBoundsException
}

The Type Safety Argument for Checked Exceptions

At their core, checked exceptions are a way of encoding potential failure modes into the type system via method signatures. This makes certain failure cases part of the API contract, forcing client code to explicitly handle these cases.

Consider this method signature:

public byte[] readFileContents(String filePath) throws IOException

The throws IOException clause tells us something critical: this method might fail in ways related to IO operations. The compiler ensures you can't simply ignore this fact. You must either:

  1. Handle the exception with a try-catch block
  2. Propagate it by declaring it in your own method signature

This type-level representation of potential failures aligns perfectly with principles of modern type-safe programming.

Automatic Propagation: A Hidden Advantage

One often overlooked advantage of Java's checked exceptions is their automatic propagation. Once you declare a method as throws IOException, any exception that occurs is automatically propagated to the caller without additional syntax.

Compare this with Rust, where you must use the ? operator every time you call a function that returns a Result:

// Rust requires explicit propagation with ? for each call
fn read_and_process(path: &str) -> Result<(), std::io::Error> {
    let content = std::fs::read_to_string(path)?;
    process_content(&content)?;
    Ok(())
}

// Java automatically propagates exceptions once declared
void readAndProcess(String path) throws IOException {
    String content = Files.readString(Path.of(path));
    processContent(content); // If this throws IOException, it's automatically propagated
}

In complex methods with many potential failure points, Java's approach leads to cleaner code by eliminating the need for repetitive error propagation markers.

Modern Parallels: Result Types in Rust and Haskell

The approach of encoding failure possibilities in the type system has been adopted by many modern languages, most notably Rust with its Result<T, E> type and Haskell with its Either a b type.

In Rust:

fn read_file_contents(file_path: &str) -> Result<Vec<u8>, std::io::Error> {
    std::fs::read(file_path)
}

When calling this function, you can't just ignore the potential for errors—you need to handle both the success case and the error case, often using the ? operator or pattern matching.

In Haskell:

readFileContents :: FilePath -> IO (Either IOException ByteString)
readFileContents path = try $ BS.readFile path

Again, the caller must explicitly deal with both possible outcomes.

This is fundamentally the same insight that motivated Java's checked exceptions: make failure handling explicit in the type system.

Valid Criticisms of Checked Exceptions

If checked exceptions are conceptually similar to these widely-praised error handling mechanisms, why have they fallen out of favor? There are several legitimate criticisms:

1. Excessive Boilerplate in the Call Chain

The most common complaint is the boilerplate required when propagating exceptions up the call stack:

void methodA() throws IOException {
    methodB();
}

void methodB() throws IOException {
    methodC();
}

void methodC() throws IOException {
    // Actual code that might throw IOException
}

Every method in the chain must declare the same exception, creating repetitive code. While automatic propagation works well within a method, the explicit declaration in method signatures creates overhead.

2. Poor Integration with Functional Programming

Java 8 introduced lambdas and streams, but checked exceptions don't play well with them:

// Won't compile because map doesn't expect functions that throw checked exceptions
List<String> fileContents = filePaths.stream()
    .map(path -> Files.readString(Path.of(path))) // Throws IOException
    .collect(Collectors.toList());

This forces developers to use awkward workarounds:

List<String> fileContents = filePaths.stream()
    .map(path -> {
        try {
            return Files.readString(Path.of(path));
        } catch (IOException e) {
            throw new UncheckedIOException(e); // Wrap in an unchecked exception
        }
    })
    .collect(Collectors.toList());

3. Interface Evolution Problems

Adding a checked exception to an existing method breaks all implementing classes and calling code. This makes evolving interfaces over time difficult, especially for widely-used libraries and frameworks.

4. Catch-and-Ignore Anti-Pattern

The strictness of checked exceptions can lead to the worst possible outcome—developers simply catching and ignoring exceptions to make the compiler happy:

try {
    // Code that might throw
} catch (Exception e) {
    // Do nothing or just log
}

This is worse than having no exception checking at all because it provides a false sense of security.

Improving Checked Exceptions Without Abandoning Them

Rather than abandoning checked exceptions entirely, Java could enhance the existing system to address these legitimate concerns. Here are some potential improvements that preserve the type safety benefits while addressing the practical problems:

1. Allow lambdas to declare checked exceptions

One of the biggest pain points with checked exceptions today is their incompatibility with functional interfaces. Consider how much cleaner this would be:

// Current approach - forced to handle or wrap exceptions inline
List<String> contents = filePaths.stream()
    .map(path -> {
        try {
            return Files.readString(Path.of(path));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    })
    .collect(Collectors.toList());

// Potential future approach - lambdas can declare exceptions
List<String> contents = filePaths.stream()
    .map((String path) throws IOException -> Files.readString(Path.of(path)))
    .collect(Collectors.toList());

This would require updating functional interfaces to support exception declarations:

@FunctionalInterface
public interface Function<T, R, E extends Exception> {
    R apply(T t) throws E;
}

2. Generic exception types in throws clauses

Another powerful enhancement would be allowing generic type parameters in throws clauses:

public <E extends Exception> void processWithException(Supplier<Void, E> supplier) throws E {
    supplier.get();
}

This would enable much more flexible composition of methods that work with different exception types, bringing some of the flexibility of Rust's Result<T, E> to Java's existing exception system.

3. Better support for exception handling in functional contexts

Unlike Rust which requires the ? operator for error propagation, Java already automatically propagates checked exceptions when declared in the method signature. What Java needs instead is better support for checked exceptions in functional contexts:

// Current approach for handling exceptions in streams
List<String> contents = filePaths.stream()
    .map(path -> {
        try {
            return Files.readString(Path.of(path));
        } catch (IOException e) {
            throw new RuntimeException(e); // Lose type information
        }
    })
    .collect(Collectors.toList());

// Hypothetical improved API
List<String> contents = filePaths.stream()
    .mapThrowing(path -> Files.readString(Path.of(path))) // Preserves checked exception
    .onException(IOException.class, e -> logError(e))
    .collect(Collectors.toList());

4. Integration with Optional<T> and Stream<T> APIs

The standard library could be enhanced to better support operations that might throw checked exceptions:

// Hypothetical API
Optional<String> content = Optional.ofThrowable(() -> Files.readString(Path.of("file.txt")));
content.ifPresentOrElse(
    this::processContent,
    exception -> log.error("Failed to read file", exception)
);

Comparison with Other Languages' Approaches

It's worth examining how other languages have addressed the error handling problem:

Rust's Result<T, E> and ? operator

Rust's approach using Result<T, E> and the ? operator shows how propagation can be made concise while keeping the type safety benefits. The ? operator automatically unwraps a successful result or returns the error to the caller, making propagation more elegant.

However, Rust's approach requires explicit propagation at each step, which can be more verbose than Java's automatic propagation in certain scenarios.

Kotlin's Approach

Kotlin made all exceptions unchecked but provides functional constructs like runCatching that bring back some type safety in a more modern way:

val result = runCatching {
    Files.readString(Path.of("file.txt"))
}

result.fold(
    onSuccess = { content -> processContent(content) },
    onFailure = { exception -> log.error("Failed to read file", exception) }
)

This approach works well with Kotlin's functional programming paradigm but lacks compile-time enforcement.

Scala's Try[T], Either[A, B], and Effect Systems

Scala offers Try[T], Either[A, B], and various effect systems that encode errors in the type system while integrating well with functional programming:

import scala.util.Try

val fileContent: Try[String] = Try {
  Source.fromFile("file.txt").mkString
}

fileContent match {
  case Success(content) => processContent(content)
  case Failure(exception) => log.error("Failed to read file", exception)
}

This approach preserves type safety while fitting well with Scala's functional paradigm.

Conclusion

Java's checked exceptions were a pioneering attempt to bring type safety to error handling. While the implementation has shortcomings, the core concept aligns with modern type-safe approaches to error handling in languages like Rust and Haskell.

Copying Rust's Result<T, E> might seem like the obvious solution, but it would represent a radical departure from Java's established paradigms. Instead, targeted enhancements to the existing checked exceptions system—like allowing lambdas to declare exceptions and supporting generic exception types—could preserve Java's unique approach while addressing its practical limitations.

The beauty of such improvements is that they'd maintain backward compatibility while making checked exceptions work seamlessly with modern Java features like lambdas and streams. They would acknowledge that the core concept of checked exceptions was sound—the problem was in the implementation details and their interaction with newer language features.

So rather than abandoning checked exceptions entirely, perhaps we should recognize them as a forward-thinking feature that was implemented before its time. As Java continues to evolve, we have an opportunity to refine this system rather than replace it.

In the meantime, next time you're tempted to disparage checked exceptions, remember: they're not just an annoying Java quirk—they're an early attempt at the same type safety paradigm that newer languages now implement with much celebration.

What do you think? Could these improvements make checked exceptions viable for modern Java development? Or is it too late to salvage this controversial feature? I'm interested in hearing your thoughts in the comments.

Read more →
0

애플리케이션 개발 측면에서 본 Drizzle ORM 대 Kysely 비교

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

TypeScript로 백엔드 서버를 개발하면서 적절한 ORM 선택은 항상 중요한 결정 중 하나입니다. 최근 제 프로젝트에서 Drizzle ORM과 Kysely를 모두 사용해 볼 기회가 있었는데, 개인적으로는 Drizzle ORM이 더 편리하고 생산성이 높았던 경험을 공유하고자 합니다.

두 ORM에 대한 간략한 소개

Drizzle ORM은 TypeScript용 ORM으로, 타입 안전성과 직관적인 API를 강점으로 내세우고 있습니다. 스키마 정의부터 마이그레이션, 쿼리 빌더까지 풀스택 개발 경험을 제공합니다.

Kysely는 “타입 안전한 SQL 쿼리 빌더”로 자신을 소개하며, 타입스크립트의 타입 시스템을 활용해 쿼리 작성 시 타입 안전성을 보장합니다.

두 도구 모두 훌륭하지만, 제 개발 경험에 비추어 볼 때 Drizzle ORM이 몇 가지 측면에서 더 편리했습니다.

Drizzle ORM을 선호하게 된 이유

스키마 정의의 직관성

Drizzle ORM의 스키마 정의 방식은 매우 직관적이고 선언적입니다:

import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').unique().notNull(),
  age: integer('age')
});

Drizzle ORM은 이 스키마 정의로부터 자동으로 CREATE TABLE SQL을 생성할 수 있어, 스키마와 코드가 항상 동기화되어 있습니다.

반면 Kysely는 타입 정의에 더 중점을 두고 있어 스키마와 타입 정의가 분리되는 경향이 있습니다:

interface Database {
  users: {
    id: Generated<number>;
    name: string;
    email: string;
    age: number | null;
  };
}

이 타입 정의는 TypeScript 코드에서 타입 안전성을 제공하지만, 이 타입 정의만으로는 CREATE TABLE SQL을 생성할 수 없다는 것이 결정적인 단점입니다. 실제로 테이블을 생성하려면 별도의 SQL 스크립트나 마이그레이션 코드를 작성해야 합니다. 이는 타입과 실제 데이터베이스 스키마 간의 불일치 가능성을 높입니다.

Drizzle의 접근 방식이 데이터베이스 스키마와 TypeScript 타입을 더 긴밀하게 연결해주어 개발 과정에서 혼란을 줄여주었습니다.

마이그레이션 경험

Drizzle ORM의 마이그레이션 도구(drizzle-kit)는 정말 인상적이었습니다. 스키마 변경사항을 자동으로 감지하고 SQL 마이그레이션 파일을 생성해주는 기능이 개발 워크플로우를 크게 개선했습니다:

npx drizzle-kit generate:pg

이 명령어 하나로 스키마 변경사항에 대한 마이그레이션 파일이 생성되며, 이를 검토하고 적용하는 과정이 매우 간단했습니다.

반면 Kysely의 마이그레이션은 본질적으로 수동적입니다. 개발자가 직접 마이그레이션 파일을 작성해야 하며, 스키마 변경사항을 자동으로 감지하거나 SQL을 생성해주는 기능이 없습니다:

// Kysely의 마이그레이션 예시
async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('users')
    .addColumn('id', 'serial', (col) => col.primaryKey())
    .addColumn('name', 'text', (col) => col.notNull())
    .addColumn('email', 'text', (col) => col.unique().notNull())
    .addColumn('age', 'integer')
    .execute();
}

async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('users').execute();
}

이러한 수동 방식은 복잡한 스키마 변경에서 실수할 가능성이 높아지고, 특히 큰 프로젝트에서는 작업량이 상당히 증가할 수 있었습니다.

하지만 Kysely의 마이그레이션에도 두 가지 중요한 장점이 있습니다:

  1. TypeScript 기반 마이그레이션: Kysely의 마이그레이션 스크립트는 TypeScript로 작성되기 때문에, 마이그레이션 로직에 애플리케이션 로직을 통합할 수 있습니다. 예를 들어, S3와 같은 오브젝트 스토리지의 데이터도 함께 마이그레이트하는 복잡한 시나리오를 구현할 수 있습니다. 반면 Drizzle ORM은 SQL 기반 마이그레이션이므로 이러한 통합이 불가능합니다.

  2. 양방향 마이그레이션: Kysely는 updown 함수를 모두 정의하여 업그레이드와 다운그레이드를 모두 지원합니다. 이는 특히 팀 협업 환경에서 중요한데, 다른 개발자의 변경사항과 충돌이 발생할 경우 롤백이 필요할 수 있기 때문입니다. Drizzle ORM은 현재 업그레이드만 지원하며, 다운그레이드 기능이 없어 협업 시 불편할 수 있습니다.

참고로, Python 생태계의 SQLAlchemy 마이그레이션 도구인 Alembic은 훨씬 더 발전된 형태의 마이그레이션을 제공합니다. Alembic은 비선형적인 마이그레이션 경로(브랜치포인트 생성 가능)를 지원하여 복잡한 팀 개발 환경에서도 유연하게 대응할 수 있습니다. 이상적으로는 JavaScript/TypeScript 생태계의 ORM도 이러한 수준의 마이그레이션 도구를 제공하는 것이 바람직합니다.

관계 설정의 용이성

Drizzle ORM에서 테이블 간 관계 설정이 매우 직관적이었습니다:

import { relations } from 'drizzle-orm';

export const usersRelations = relations(users, ({ one, many }) => ({
  profile: one(profiles, {
    fields: [users.id],
    references: [profiles.userId],
  }),
  posts: many(posts)
}));

이 방식은 데이터베이스 설계의 본질적인, 관계적인 측면을 명확하게 표현해주었습니다.

쿼리 작성의 편의성과 동일 이름 칼럼 문제 처리

두 ORM 모두 쿼리 작성을 위한 API를 제공하지만, Drizzle의 접근 방식이 더 직관적이고 관계형 모델을 활용하기 쉬웠습니다:

// Drizzle ORM - db.query 방식으로 관계 활용
const result = await db.query.posts.findMany({
  where: eq(posts.published, true),
  with: {
    user: true  // 게시물 작성자 정보를 함께 조회  
  }
});

// 결과 접근이 직관적이고 타입 안전함
console.log(result[0].title);       // 게시물 제목
console.log(result[0].user.name);   // 작성자 이름 - 객체 구조로 명확하게 구분됨  
console.log(result[0].user.id);     // 작성자 ID - 게시물 ID와 이름이 같아도 문제 없음  

// Kysely
const result = await db
  .selectFrom('posts')
  .where('posts.published', '=', true)
  .leftJoin('users', 'posts.userId', 'users.id')
  .selectAll();

// 결과 접근 시 칼럼 이름 충돌 문제
console.log(result[0].id) // 오류: posts.id와 users.id 중 어떤 것인지 모호함  
console.log(result[0].name) // 오류: 둘 다 name 칼럼이 있다면 모호함  

Drizzle의 접근 방식이 테이블과 컬럼을 참조할 때 타입 안전성을 더 강력하게 보장하고, 관계를 활용한 쿼리 작성이 더 직관적이었습니다.

특히 여러 테이블 조인 시 동일한 이름의 칼럼 처리 부분에서 Drizzle ORM이 훨씬 더 편리했습니다. 이는 제 개발 경험에서 가장 중요한 차이점 중 하나였습니다.

// Drizzle ORM - 동일 이름 칼럼 처리
const result = await db.query.posts.findMany({
  with: {
    user: true  // posts.id와 users.id가 모두 있지만 자동으로 구분됨
  }
});

// 결과에 자연스럽게 접근 가능  
console.log(result[0].id);        // 게시물 ID  
console.log(result[0].user.id);   // 사용자 ID - 명확하게 구분됨  
console.log(result[0].user.name); // 사용자 이름  

// Kysely - 동일 이름 칼럼 처리를 위해 별칭 필요
const result = await db
  .selectFrom('posts')
  .leftJoin('users', 'posts.userId', 'users.id')
  .select([
    'posts.id as postId',       // 별칭 필수  
    'posts.title',
    'posts.content',
    'users.id as userId',       // 별칭 필수  
    'users.name as userName',   // 칼럼 이름이 같을 수 있으므로 별칭 필수  
    'users.email as userEmail'  // 일관성을 위해 모든 사용자 관련 칼럼에 접두어 필요  
  ]);

// 별칭을 통한 접근
console.log(result[0].postId);    // 게시물 ID
console.log(result[0].userId);    // 사용자 ID
console.log(result[0].userName);  // 사용자 이름

Drizzle ORM은 테이블과 칼럼을 객체로 참조하기 때문에, 동일한 이름의 칼럼이 있어도 자연스럽게 계층 구조로 처리되며 타입 추론도 정확하게 작동합니다. 반면 Kysely에서는 문자열 기반 접근 방식 때문에 별칭을 수동으로 지정해야 하는 경우가 많았고, 복잡한 조인에서 이런 작업이 번거로워졌습니다. 특히 여러 테이블에 같은 이름의 칼럼이 많을수록 모든 칼럼에 명시적인 별칭을 지정해야 하는 불편함이 있었습니다.

또한 Drizzle ORM은 결과 타입을 자동으로 정확하게 추론해주어 별도의 타입 지정 없이도 안전하게 결과를 사용할 수 있었습니다.

Kysely의 장점

물론 Kysely도 여러 강점이 있습니다:

  1. 더 가벼운 구조: 필요한 기능만 포함할 수 있는 모듈화된 구조
  2. SQL에 더 가까운 접근: SQL 구문에 매우 충실한 API 설계
  3. 유연성: 복잡한 쿼리에서 때로 더 유연한 작성이 가능

또한 앞서 언급했듯이, Kysely의 TypeScript 기반 마이그레이션과 양방향(up/down) 마이그레이션 지원은 특정 상황에서 Drizzle ORM보다 우위에 있는 기능입니다.

SQLAlchemy와의 비교 및 앞으로의 기대

JavaScript/TypeScript 생태계의 ORM을 이야기하기 전에, 여러 언어 중에서도 Python의 SQLAlchemy는 특별한 위치를 차지합니다. 개인적으로 여태 사용해본 다양한 언어의 ORM 중에서 SQLAlchemy가 가장 기능이 풍부하고 강력하다고 느꼈습니다. 복잡한 쿼리 구성, 고급 관계 매핑, 트랜잭션 관리, 이벤트 시스템 등 SQLAlchemy의 기능은 정말 방대합니다.

Drizzle ORM은 JavaScript 생태계에서 매우 인상적인 발전을 이루었지만, 아직 SQLAlchemy의 경지에는 이르지 못했다고 생각합니다. 특히 다음과 같은 부분에서 SQLAlchemy의 성숙도와 기능 풍부함이 돋보입니다:

  • 복잡한 서브쿼리와 윈도우 함수 지원
  • 다양한 이벤트 리스너와 훅
  • 다양한 상속 전략
  • 복잡한 트랜잭션 관리와 세션 관리
  • 대규모 프로젝트에서 검증된 안정성
  • Alembic을 통한 비선형적 마이그레이션 지원
  • 놀라울 정도로 방대하고 상세한 문서화

결론

두 ORM 모두 훌륭한 도구이지만, 제 개발 스타일과 프로젝트 요구사항에는 Drizzle ORM이 더 잘 맞았습니다. 특히 스키마 정의의 직관성, 강력한 마이그레이션 도구, 그리고 전반적인 개발자 경험 측면에서 Drizzle ORM이 더 생산적인 개발을 가능하게 해주었습니다.

동일 이름 칼럼 처리와 같은 실질적인 문제에서 Drizzle ORM의 객체 기반 접근 방식이 가져다주는 편리함은 실제 프로젝트에서 큰 차이를 만들었습니다.

ORM 선택은 결국 프로젝트 특성과 개인 선호도에 크게 좌우됩니다. 새로운 프로젝트를 시작한다면 두 도구 모두 간단히 테스트해보고 자신의 워크플로우에 더 적합한 것을 선택하는 것이 좋겠지만, 제 경우에는 Drizzle ORM이 명확한 승자였습니다.

앞으로 Drizzle ORM이 더욱 발전하여 SQLAlchemy 수준의 풍부한 기능과 유연성을 제공하게 되길 바랍니다. JavaScript/TypeScript 생태계에도 그런 수준의 강력한 ORM이 있으면 좋겠습니다. 다행히도 Drizzle ORM은 계속해서 발전하고 있으며, 그 발전 속도를 보면 기대가 큽니다.

여러분의 경험은 어떤가요? 다른 ORM 도구나 언어를 사용해보셨다면 의견을 공유해주세요!

Read more →
0

Hackers' Pub에서 DOT 언어(Graphviz)로 다이어그램 그리기

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

Hackers' Pub의 숨겨진 기능 중 하나는 Graphviz의 DOT 언어를 지원한다는 것입니다. 예를 들어, 다음과 같은 다이어그램을 그릴 수 있습니다:

SimpleActivityPub server_a 서버 A (Mastodon) server_b 서버 B (Hackers' Pub) server_a->server_b ActivityStreams 데이터 전송 (HTTP POST) server_b->server_a 응답 및 상호작용 (HTTP POST)

Graphviz를 이용하는 법은 간단합니다. Markdown의 코드 블럭 문법 안에 DOT 언어로 다이어그램을 기술하신 뒤, 코드 블럭의 언어 태그에 graphviz를 붙이시면 됩니다. 위에서 예를 든 다이어그램은 Markdown에서 아래와 같이 쓰면 됩니다:

```graphviz
digraph SimpleActivityPub {
    graph [rankdir=LR, fontname="sans-serif", bgcolor="white"];
    node [fontname="sans-serif", shape=box, style="rounded,filled"];
    edge [fontname="sans-serif"];
    
    server_a [label="서버 A\n(Mastodon)", fillcolor="#AED6F1"];
    server_b [label="서버 B\n(Hackers' Pub)", fillcolor="#A3E4D7"];
    
    server_a -> server_b [label="ActivityStreams 데이터 전송\n(HTTP POST)", color="red"];
    server_b -> server_a [label="응답 및 상호작용\n(HTTP POST)", color="blue"];
}
```

참고로 Graphviz는 긴 게시글 뿐만 아니라 단문에서도 똑같이 지원합니다.

Read more →
1

Monads: Beyond Simple Analogies—Reflections on Functional Programming Paradigms

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

While exploring functional programming languages, I've been reflecting on how different communities approach similar concepts. One pattern that seems particularly fascinating is how Haskell and OCaml communities differ in their embrace of monads as an abstraction tool.

The Elegant Power of Monads in Haskell

It's common to hear monads explained through analogies to concepts like JavaScript's Promise or jQuery chains. While these comparisons provide an entry point, they might miss what makes monads truly beautiful and powerful in Haskell's ecosystem.

The real strength appears to lie in the Monad typeclass itself. This elegant abstraction allows for creating generic functions and types that work with any type that shares the monad property. This seems to offer a profound unification of concepts that might initially appear unrelated:

  • You can write code once that works across many contexts (Maybe, [], IO, State, etc.)
  • Generic functions like sequence, mapM, and others become available across all monadic types
  • The same patterns and mental models apply consistently across different computational contexts

For example, a simple conditional function like this works beautifully in any monadic context:

whenM :: Monad m => m Bool -> m () -> m ()
whenM condition action = do
  result <- condition
  if result then action else return ()

Whether dealing with potentially missing values, asynchronous operations, or state transformations, the same function can be employed without modification. There's something genuinely satisfying about this level of abstraction and reuse.

OCaml's Different Approach

Interestingly, the OCaml community seems less enthusiastic about monads as a primary abstraction tool. This might stem from several factors related to language design:

Structural Differences

OCaml lacks built-in typeclass support, relying instead on its module system and functors. While powerful in its own right, this approach might not make monad abstractions feel as natural or convenient:

(* OCaml monad implementation requires more boilerplate *)
module type MONAD = sig
  type 'a t
  val return : 'a -> 'a t
  val bind : 'a t -> ('a -> 'b t) -> 'b t
end

module OptionMonad : MONAD with type 'a t = 'a option = struct
  type 'a t = 'a option
  let return x = Some x
  let bind m f = match m with
    | None -> None
    | Some x -> f x
end

OCaml also doesn't offer syntactic sugar like Haskell's do notation, which makes monadic code in Haskell considerably more readable and expressive:

-- Haskell's elegant do notation
userInfo = do
  name <- getLine
  age <- readLn
  return (name, age)

Compared to the more verbose OCaml equivalent:

let user_info =
  get_line >>= fun name ->
  read_ln >>= fun age ->
  return (name, age)

The readability difference becomes even more pronounced in more complex monadic operations.

Philosophical Differences

Beyond syntax, the languages differ in their fundamental approach to effects:

  • Haskell is purely functional, making monads essential for managing effects in a principled way
  • OCaml permits direct side effects, often making monadic abstractions optional

This allows OCaml programmers to write more direct code when appropriate:

(* Direct style in OCaml *)
let get_user_info () =
  print_string "Name: ";
  let name = read_line () in
  print_string "Age: ";
  let age = int_of_string (read_line ()) in
  (name, age)

OCaml's approach might favor pragmatism and directness in many cases, with programmers often preferring:

  • Direct use of option and result types
  • Module-level abstractions through functors
  • Continuation-passing style when needed

While this directness can be beneficial for immediate readability, it might come at the cost of some of the elegant uniformity that Haskell's monadic approach provides.

Reflections on Language Design

These differences highlight how programming language design shapes the idioms and patterns that emerge within their communities. Neither approach is objectively superior—they represent different philosophies about abstraction, explicitness, and the role of the type system.

Haskell's approach encourages a high level of abstraction and consistency across different computational contexts, which can feel particularly satisfying when working with complex, interconnected systems. There's something intellectually pleasing about solving a problem once and having that solution generalize across many contexts.

OCaml often favors more direct solutions that might be easier to reason about locally, though potentially at the cost of less uniformity across the codebase. This approach has its own virtues, particularly for systems where immediate comprehensibility is paramount.

After working with both paradigms, I find myself drawn to the consistent abstractions that Haskell's approach provides, while still appreciating the pragmatic clarity that OCaml can offer in certain situations. The typeclasses and syntactic support in Haskell seem to unlock a particularly elegant way of structuring code that, while perhaps requiring a steeper initial learning curve, offers a uniquely satisfying programming experience.

What patterns have you noticed in how different programming language communities approach similar problems? And have you found yourself drawn to the elegant abstractions of Haskell or the pragmatic approach of OCaml?

Read more →
0

Hackers' Pub Introduces Flexible Username Changes: Breaking the Fediverse Norm

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

Hackers' Pub is a community-focused platform where programmers and technology enthusiasts share knowledge and experiences. As an ActivityPub-enabled social network, it allows users to connect with others across the broader fediverse ecosystem, bringing technical discussions and insights directly to followers' feeds.

In the fediverse landscape, your username is typically set in stone once chosen. Most ActivityPub-powered platforms like Mastodon, Pleroma, and others enforce this permanence as a fundamental design principle. However, Hackers' Pub is charting a different course with a more flexible approach to digital identity.

One-Time Username Change: Freedom with Responsibility

Unlike most fediverse platforms, Hackers' Pub now allows users to change their username (the part before the @ in your Fediverse handle) exactly once during the lifetime of their account. This policy acknowledges that people grow, interests evolve, and the username that seemed perfect when you joined might not represent who you are today.

This one-time change limit strikes a careful balance—offering flexibility while maintaining the stability and reliability that's essential for a federated network.

Username Recycling: New Opportunities

When you change your username on Hackers' Pub, your previous username becomes available for other users to claim. This recycling mechanism creates new opportunities for meaningful usernames to find their most fitting owners, rather than remaining permanently locked to accounts that no longer use them.

For newcomers to the platform, this means a wider selection of desirable usernames might become available over time—something virtually unheard of in the traditional fediverse ecosystem.

Link Preservation: Maintaining Digital History

Worried about broken links after changing your username? Hackers' Pub has implemented a thoughtful solution. All permalinks containing your original username will continue to function until someone else claims that username. This approach helps preserve the web of connections and conversations that make the fediverse valuable.

This temporary preservation period gives your connections time to adjust to your new identity while preventing immediate link rot across the federation.

The Technical Foundation: ActivityPub Actor URIs

What enables Hackers' Pub to offer username changes while other fediverse platforms can't? The answer lies in how actor identities are implemented at the protocol level.

Hackers' Pub uses UUID-based actor URIs that don't contain the username. For example, a user with handle @hongminhee has an underlying ActivityPub actor URI that looks like https://hackers.pub/ap/actors/019382d3-63d7-7cf7-86e8-91e2551c306c. Since the username isn't part of this permanent identifier, it can be changed without breaking federation connections.

This contrasts sharply with platforms like Mastodon, where a user @hongminhee has an actor URI of https://mastodon.social/users/hongminhee. With the username embedded directly in the URI, changing it would break all federation connections, which is why these platforms don't allow username changes.

This architectural choice gives Hackers' Pub the technical flexibility to implement username changes while maintaining account continuity across the fediverse.

GitHub-Inspired Approach

Those familiar with GitHub might recognize this model—Hackers' Pub has adapted GitHub's username change policy for the fediverse context. This approach brings the best of both worlds: the option for identity evolution from centralized platforms and the federation benefits of the fediverse.

What This Means for Users

For Hackers' Pub users, this policy offers a significant advantage over other fediverse instances:

  • You can correct an unfortunate username choice
  • Your online identity can evolve as you do
  • Your content history remains intact during the transition
  • You maintain your social connections despite the change

The Future of Fediverse Identity

Hackers' Pub's username policy represents an interesting experiment in the fediverse—testing whether more flexible identity management can coexist with the stability needed for federation. If successful, we might see other platforms adopt similar approaches, creating a more adaptable yet still interconnected social web.

For now, users should consider this policy a compelling reason to choose Hackers' Pub as their fediverse home, especially if username flexibility matters to their online experience.


Hackers' Pub is currently in invitation-only beta. If you're interested in trying out the platform and its unique username policy, please leave your email address in the comments below. We'll add you to the allowlist, enabling you to sign up directly on the website. Note that this doesn't involve sending invitation emails—your address will simply be approved for registration when you visit the signup page.

Read more →
0

LogTape 0.9.0 Released: Synchronous Configuration and Better Runtime Compatibility

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

We're excited to announce the release of LogTape 0.9.0! This version brings important improvements to make LogTape more flexible across different JavaScript environments while simplifying configuration for common use cases.

What's New

  • Synchronous Configuration API: Added new synchronous configuration functions for environments where async operations aren't needed or desired
  • Improved Runtime Compatibility: Moved file-system dependent components to a separate package for better cross-runtime support

New Features

Synchronous Configuration API: Simplifying Your Setup

Added synchronous versions of the configuration functions:

  • configureSync(): Configure LogTape synchronously
  • disposeSync(): Dispose LogTape resources synchronously
  • resetSync(): Reset LogTape configuration synchronously

These functions offer a simpler API for scenarios where async operations aren't needed, allowing for more straightforward code without awaiting promises. Note that these functions cannot use sinks or filters that require asynchronous disposal (such as stream sinks), but they work perfectly for most common logging configurations.

import { configureSync, getConsoleSink } from "@logtape/logtape";

configureSync({ 
  sinks: {
    console: getConsoleSink(),
  },
  loggers: [
    {
      category: "my-app",
      lowestLevel: "info",
      sinks: ["console"],
    },
  ],
});

Console Sink Enhancements

  • Added ConsoleSinkOptions.levelMap option for customizing how log levels are mapped to console methods

Breaking Changes

File Sinks Moved to Separate Package: Better Cross-Platform Support

To improve runtime compatibility, file-related sinks have been moved to the @logtape/file package:

  • Moved getFileSink() function to @logtape/file package
  • Moved FileSinkOptions interface to @logtape/file package
  • Moved getRotatingFileSink() function to @logtape/file package
  • Moved RotatingFileSinkOptions interface to @logtape/file package

This architectural change ensures the core @logtape/logtape package is fully compatible with all JavaScript runtimes, including browsers and edge functions, without introducing file system dependencies. You'll now enjoy better compatibility with bundlers like Webpack, Rollup, and Vite that previously had issues with the file system imports.

Migration Guide

If you were using file sinks, update your imports:

// Before
import { getFileSink, getRotatingFileSink } from "@logtape/logtape";

// After
import { getFileSink, getRotatingFileSink } from "@logtape/file";

Don't forget to install the new package:

# For npm, pnpm, Yarn, Bun
npm add @logtape/file

# For Deno
deno add jsr:@logtape/file

Looking Forward

This release represents our ongoing commitment to making LogTape the most flexible and developer-friendly logging solution for JavaScript and TypeScript applications. We're continuing to improve performance and extend compatibility across the JavaScript ecosystem.

Contributors

Special thanks to Murph Murphy for their valuable contribution to this release.


As always, we welcome your feedback and contributions! Feel free to open issues or pull requests on our GitHub repository.

Happy logging!

Read more →
0

연합우주(fediverse)와 ActivityPub 프로토콜 이해하기: 개발자를 위한 가이드

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

연합우주란 무엇일까?

X(구 Twitter)나 Instagram 같은 중앙화된 소셜 미디어에 지치셨나요? 데이터 프라이버시, 알고리즘 추천, 그리고 끊임없는 광고가 걱정되시나요? 여기 대안이 있습니다. 바로 연합우주(fediverse)입니다.

페디버스(fediverse)는 “federated”(연합된)와 “universe”(우주)를 합친 말로, 한국어권에서는 주로 “연합우주”라고 불립니다. 연합우주는 하나의 거대한 플랫폼이 아닌, 서로 대화할 수 있는 독립적인 서버(인스턴스)들의 네트워크입니다.

이게 어떻게 가능할까요? 바로 ActivityPub이라는 프로토콜 덕분입니다. 이 프로토콜은 서로 다른 소셜 미디어 플랫폼이 정보를 교환할 수 있게 해주는 공통 언어 같은 것입니다.

연합우주는 어떻게 작동하나요?

연합우주를 이해하는 가장 쉬운 방법은 이메일 시스템과 비교하는 것입니다.

Gmail 사용자가 네이버 메일 사용자에게 이메일을 보낼 수 있는 것처럼, Mastodon 사용자는 Misskey나 PeerTube 사용자와 소통할 수 있습니다. (Mastodon, Misskey, PeerTube가 무엇인지는 아래에서 설명하겠습니다. Gmail과 네이버처럼 서로 다른 서비스라고 보시면 됩니다.) 이것이 가능한 이유는 이 서비스들이 모두 같은 언어인 ActivityPub 프로토콜로 대화하기 때문입니다.

연합우주에서 사용자 ID는 @사용자명@인스턴스.도메인 형식으로 되어 있습니다. 이메일 주소와 매우 비슷하죠? 예를 들면:

  • @honggildong@mastodon.social: mastodon.social 인스턴스 사용자
  • @kimcheolsu@pixelfed.social: pixelfed.social 인스턴스 사용자
  • @leeyeonghui@misskey.io: misskey.io 인스턴스 사용자

연합우주의 다양한 플랫폼 둘러보기

연합우주는 마치 여러 행성으로 이루어진 태양계 같습니다. 각 행성(플랫폼)은 고유한 특성을 가지고 있지만, 모두 같은 우주(연합우주)에 속해 있죠. 아래 표에서 주요 플랫폼들을 살펴봅시다:

플랫폼 설명 주요 인스턴스 특징
Mastodon X(구 Twitter)와 유사한 마이크로블로깅 플랫폼 • mastodon.social (공식 인스턴스)
• 우리.인생 (한국 중심)
500자 제한의 짧은 게시물, 해시태그, 컨텐츠 경고 기능
Misskey 일본에서 개발된 고도로 커스터마이징 가능한 마이크로블로깅 플랫폼 • misskey.io (가장 인기 있는 일본 인스턴스)
• 스텔라 (한국 중심)
리액션, 게임, 채팅 등 다양한 기능, 높은 커스터마이징 가능성
Pixelfed Instagram과 유사한 이미지 공유 플랫폼 • pixelfed.social (공식 인스턴스)
• 추억:사진 (한국 중심)
스토리, 필터, 발견 기능
PeerTube YouTube와 유사한 비디오 호스팅 플랫폼 • PeerTube.TV P2P 기술로 비디오 스트리밍, 채널, 재생목록
WriteFreely 미니멀한 블로그 플랫폼 • write.as Markdown 지원, 심플한 디자인
Lemmy Reddit과 유사한 링크 애그리게이터 및 토론 플랫폼 • lemmy.ml
• YuruLemmy (한국 중심)
커뮤니티(서브레딧과 유사), 투표, 토론

플랫폼 vs 인스턴스: 무슨 차이가 있을까?

연합우주를 이해할 때 흔히 혼동되는 개념이 있습니다. 바로 플랫폼(소프트웨어)과 인스턴스(서버)의 차이인데요.

플랫폼은 Mastodon, Misskey, Pixelfed와 같은 소프트웨어 자체를 의미합니다. 이들은 오픈 소스 소프트웨어로, 누구나 다운로드받아 설치할 수 있습니다.

인스턴스는 그 소프트웨어를 실행하는 개별 서버를 말합니다. mastodon.social과 우리.인생은 모두 Mastodon 플랫폼을 실행하는 별도의 인스턴스입니다.

Meta의 Threads 같은 일부 서비스는 플랫폼과 인스턴스가 동일합니다. 하지만 대부분의 연합우주 서비스는 여러 인스턴스로 구성되어 있습니다.

연합우주의 매력 포인트

연합우주가 갖는 몇 가지 매력적인 특징이 있습니다:

  1. 탈중앙화: 특정 기업이 모든 데이터와 규칙을 통제하지 않습니다. 각 인스턴스는 자체 규칙을 가질 수 있습니다.
  2. 데이터 주권: 자신의 데이터에 대한 더 많은 통제권을 가질 수 있습니다.
  3. 검열 저항성: 한 인스턴스가 차단되더라도 다른 인스턴스로 쉽게 이동할 수 있습니다.
  4. 커뮤니티 중심: 각 인스턴스는 특정 관심사나 지역 커뮤니티를 중심으로 형성됩니다.
  5. 다양성: 다양한 플랫폼과 인스턴스가 존재하여 선택의 폭이 넓습니다.

연합우주 시작하기

연합우주에 참여하는 것은 생각보다 쉽습니다:

  1. 자신의 관심사나 지역과 관련된 인스턴스를 선택합니다.
  2. 해당 인스턴스에 계정을 만듭니다.
  3. 다른 인스턴스의 사용자들을 팔로우하고 소통을 시작합니다!

한국 사용자라면 Mastodon 인스턴스인 우리.인생, Misskey 인스턴스인 스텔라 같은 한국어 중심 인스턴스를 추천합니다. 한국어 환경을 지원하고 한국 사용자들이 활발하게 활동하고 있어 시작하기 좋습니다.

아니면 이 글이 올라온 Hackers' Pub도 괜찮습니다. 소프트웨어 엔지니어들을 위한 소셜 미디어랍니다. 아직 개발중이라 공개적으로 가입을 받고 있지는 않습니다만, 홍민희에게 연락 주시면 계정을 생성해 드릴 수 있습니다.

ActivityPub: 연합우주의 심장

이제 개발자 관점에서 ActivityPub이 어떻게 작동하는지 자세히 살펴보겠습니다.

ActivityPub은 W3C에서 권장하는 표준 프로토콜로, 분산 소셜 네트워킹의 기반이 됩니다. ActivityStreams 2.0 데이터 형식을 기반으로 하며, 서로 다른 서버 간에 정보를 교환하는 방법을 정의합니다.

ActivityPub의 핵심 개념

ActivityPub은 몇 가지 핵심 개념으로 구성됩니다:

  1. 액터(actor): 사용자, 그룹 등 행동을 수행할 수 있는 주체입니다. 각 액터는 고유한 URL을 가지며, 수신함(inbox)과 발신함(outbox)을 가집니다.
  2. 액티비티(activity): 액터가 수행하는 행동으로, 게시물 작성, 댓글 좋아요, 다른 사용자 팔로우 등이 있습니다.
  3. 객체(object): 텍스트 게시물, 이미지, 비디오와 같이 생성되고 공유되는 콘텐츠입니다.

실제 작동 방식

홍길동(@honggildong@mastodon.social)이 게시물을 작성하고, 이영희(@leeyeonghui@misskey.io)가 이에 반응하는 과정을 살펴봅시다:

  1. 게시물 작성: 홍길동이 Mastodon에서 게시물을 작성합니다. Mastodon 서버는 이 게시물을 ActivityStreams 2.0 형식의 Create(Note) 액티비티로 변환합니다. 이 액티비티는 홍길동의 팔로워(이영희 포함)에게 전달됩니다.

  2. 게시물 수신: 이영희의 Misskey 서버는 이 액티비티를 받고 처리하여 이영희의 타임라인에 홍길동의 게시물을 표시합니다.

  3. 상호작용: 이영희가 게시물에 좋아요를 누르면, Misskey 서버는 Like(Note) 액티비티를 생성하여 홍길동의 Mastodon 서버로 보냅니다. 홍길동은 이영희가 자신의 게시물을 좋아했다는 알림을 받게 됩니다.

마치 다른 언어를 사용하는 사람들이 통역사를 통해 대화하는 것과 비슷하죠? ActivityPub이 바로 그 통역사 역할을 합니다.

ActivityPub의 실제 메시지 들여다보기

개발자로서 실제 ActivityPub 메시지가 어떻게 생겼는지 궁금하실 텐데요. 몇 가지 예시를 살펴봅시다:

1. 사용자 프로필(액터) 정보

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1"
  ],
  "id": "https://mastodon.social/users/honggildong",
  "type": "Person",
  "preferredUsername": "honggildong",
  "name": "홍길동",
  "summary": "연합우주의 개척자",
  "inbox": "https://mastodon.social/users/honggildong/inbox",
  "outbox": "https://mastodon.social/users/honggildong/outbox",
  "followers": "https://mastodon.social/users/honggildong/followers",
  "following": "https://mastodon.social/users/honggildong/following",
  "publicKey": {
    "id": "https://mastodon.social/users/honggildong#main-key",
    "owner": "https://mastodon.social/users/honggildong",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
  },
  "icon": {
    "type": "Image",
    "mediaType": "image/jpeg",
    "url": "https://mastodon.social/system/accounts/avatars/000/000/001/original/avatar.jpg"
  }
}

이 JSON 데이터는 홍길동의 프로필 정보를 담고 있습니다. 사용자 이름, 소개, 프로필 사진 URL, 그리고 중요한 inboxoutbox URL이 포함되어 있죠.

2. 게시물 작성 액티비티

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://mastodon.social/users/honggildong/statuses/123456/activity",
  "type": "Create",
  "actor": "https://mastodon.social/users/honggildong",
  "published": "2025-02-21T14:30:00Z",
  "to": [
    "https://www.w3.org/ns/activitystreams#Public"
  ],
  "cc": [
    "https://mastodon.social/users/honggildong/followers"
  ],
  "object": {
    "id": "https://mastodon.social/users/honggildong/statuses/123456",
    "type": "Note",
    "content": "<p>연합우주에 오신 것을 환영합니다! #fediverse #연합우주</p>",
    "published": "2025-02-21T14:30:00Z",
    "attributedTo": "https://mastodon.social/users/honggildong",
    "to": [
      "https://www.w3.org/ns/activitystreams#Public"
    ],
    "cc": [
      "https://mastodon.social/users/honggildong/followers"
    ],
    "tag": [
      {
        "type": "Hashtag",
        "href": "https://mastodon.social/tags/fediverse",
        "name": "#fediverse"
      },
      {
        "type": "Hashtag",
        "href": "https://mastodon.social/tags/연합우주",
        "name": "#연합우주"
      }
    ]
  }
}

이것은 홍길동이 게시물을 작성했을 때 생성되는 Create(Note) 액티비티입니다. 게시물 내용, 해시태그, 공개 범위 등이 포함되어 있습니다.

3. 팔로우 액티비티

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://misskey.io/users/leeyeonghui/follow/1234",
  "type": "Follow",
  "actor": "https://misskey.io/users/leeyeonghui",
  "object": "https://mastodon.social/users/honggildong"
}

이영희가 홍길동을 팔로우할 때 생성되는 Follow 액티비티입니다. 단순하죠?

ActivityPub 서버 구현하기: 개발자를 위한 팁

직접 ActivityPub 서버를 구현하고 싶다면 다음 단계를 따라야 합니다:

  1. 액터 구현: 사용자 프로필 정보를 ActivityStreams 형식으로 제공합니다.
  2. 수신함과 발신함 설정: HTTP 엔드포인트를 만들어 액티비티를 받고 전송합니다.
  3. 서명 및 인증: HTTP Signatures를 사용하여 요청을 서명하고 검증합니다.
  4. 액티비티 처리: 다양한 액티비티 유형(Create, Follow, Like 등)을 처리하는 로직을 구현합니다.
  5. 데이터 저장: 사용자, 게시물, 액티비티 등의 정보를 데이터베이스에 저장합니다.
  6. 연합 정책 구현: 어떤 인스턴스와 연합할지, 어떤 컨텐츠를 허용할지 등을 설정합니다.

개발을 시작하기 전에 Mastodon, Misskey 같은 기존 구현체의 코드를 살펴보는 것이 도움이 됩니다. 처음부터 모든 것을 구현하는 것보다 Fedify 같은 프레임워크를 활용하는 것도 좋은 방법입니다.

WebFinger: 사용자를 찾는 방법

연합우주에서 @leeyeonghui@misskey.io 같은 사용자 ID를 어떻게 실제 ActivityPub 액터 URL로 변환할까요? 그 비밀은 WebFinger 프로토콜에 있습니다:

GET https://misskey.io/.well-known/webfinger?resource=acct:leeyeonghui@misskey.io

이 요청을 보내면 서버는 다음과 같은 응답을 반환합니다:

{
  "subject": "acct:leeyeonghui@misskey.io",
  "links": [
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://misskey.io/users/leeyeonghui"
    }
  ]
}

이제 https://misskey.io/users/leeyeonghui URL을 통해 사용자의 전체 프로필 정보를 얻을 수 있습니다. 마치 전화번호부에서 이름으로 전화번호를 찾는 것과 비슷하죠!

연합우주의 도전 과제와 미래

연합우주는 계속 성장하고 있지만, 몇 가지 도전 과제도 있습니다:

  1. 확장성: 수많은 서버 간의 통신을 효율적으로 처리하는 것은 쉽지 않습니다.
  2. 모더레이션: 각 인스턴스가 자체 규칙을 가지므로 콘텐츠 조정에 일관성이 부족할 수 있습니다.
  3. 발견성: 중앙화된 플랫폼에 비해 새로운 사용자나 콘텐츠를 찾기 어려울 수 있습니다.
  4. 사용자 경험: 일부 플랫폼은 아직 UI/UX 측면에서 개선이 필요합니다.

그러나 Threads와 같은 주요 서비스들이 ActivityPub을 채택하기 시작하면서, 연합우주의 미래는 밝아 보입니다. 개발자로서, 이런 성장하는 생태계에 참여할 수 있는 기회가 많이 있습니다.

마무리

연합우주와 ActivityPub은 중앙화된 소셜 미디어의 대안으로서 점점 더 주목받고 있습니다. 사용자에게 더 많은 통제권을 부여하고, 다양하고 풍부한 온라인 경험을 제공하는 연합우주의 세계는 계속해서 확장되고 있습니다.

개발자로서, 여러분은 이 새로운 탈중앙화된 웹의 생태계에 기여할 수 있습니다. 기존 애플리케이션에 ActivityPub 지원을 추가하거나, 완전히 새로운 서비스를 만들거나, 현재의 도전 과제를 해결하는 솔루션을 개발할 수 있습니다.

한국 개발자들의 참여가 늘어나면 한국 사용자들을 위한 더 다양하고 풍부한 서비스가 생길 것이고, 이는 더 건강하고 다양한 인터넷 문화를 만드는 데 기여할 것입니다.

그럼, 연합우주로의 여행을 시작해 보시는 건 어떨까요?

Read more →
3