Object Entanglement; Spooky action at a distance
Find a file
2026-06-24 01:59:29 +09:00
.vscode Spooky action at a distance 2026-06-19 14:14:04 +09:00
entangled add a hero image 2026-06-24 01:59:29 +09:00
gradle Spooky action at a distance 2026-06-19 14:14:04 +09:00
.gitattributes Spooky action at a distance 2026-06-19 14:14:04 +09:00
.gitignore Spooky action at a distance 2026-06-19 14:14:04 +09:00
AGENTS.md add a hero image 2026-06-24 01:59:29 +09:00
entanglement.png add a hero image 2026-06-24 01:59:29 +09:00
gradle.properties Spooky action at a distance 2026-06-19 14:14:04 +09:00
gradlew Spooky action at a distance 2026-06-19 14:14:04 +09:00
gradlew.bat Spooky action at a distance 2026-06-19 14:14:04 +09:00
LICENSE Spooky action at a distance 2026-06-19 14:14:04 +09:00
README.md add a hero image 2026-06-24 01:59:29 +09:00
settings.gradle.kts Spooky action at a distance 2026-06-19 14:14:04 +09:00

Property Entanglement - Spooky action at a distance

Java Gradle License Tests

개요

Spooky action at a distance

Entanglement

Entanglement는 Java를 위한 유연한 프로퍼티 바인딩 및 동기화 라이브러리입니다. 양자 얽힘(Quantum Entanglement)에서 이름을 따온 이 라이브러리는 여러 객체 간의 프로퍼티 값을 자동으로 동기화하며, 리스너, 콜백, 계산 프로퍼티, 값 검증 등 강력한 기능을 제공합니다.

Property<String> name = new ObjectProperty<>("Hello");

Property<String> anotherName = new ObjectProperty<>();
name.entangle(anotherName);  // 양방향 동기화 설정

name.set("World");
System.out.println(anotherName.get()); // World

주요 기능

기능 설명
양방향 동기화 한 프로퍼티의 변경이 연결된 모든 프로퍼티에 자동 전파
계산 프로퍼티 소스 프로퍼티로부터 값을 자동으로 계산하는 파생 프로퍼티
리스너 시스템 값 변경 시 ChangeEvent를 수신하는 콜백 등록
조건부 리스너 특정 조건을 만족할 때만 발동하는 리스너
약한 참조 리스너 GC 시 자동 해제되는 메모리 안전한 리스너
값 검증 설정 전 값의 유효성을 검사하는 조합 가능한 검증자
변경 추적 이전 값, 변경 횟수, 타임스탬프 등 변경 이력 조회
순차 실행 콜백이 큐를 통해 등록 순서대로 실행됨을 보장
순환 참조 방지 동기화 네트워크에서 무한 루프를 자동 감지 및 차단
타입 안전 모든 타입을 지원하는 제너릭 구현
스레드 안전 CopyOnWriteArrayList, volatile 등 동시성 안전한 설계

설치

Gradle (Kotlin DSL)

repositories {
    maven {
        url = uri("https://artifacts.elex-project.com/maven")
    }
}

dependencies {
    implementation("com.elex-project:entangled:1.0.0")
}

빠른 시작

기본 사용법

// 초기값과 함께 프로퍼티 생성
Property<String> name = new ObjectProperty<>("Alice");

// 값 조회
System.out.println(name.get()); // Alice

// 값 설정
name.set("Bob");
System.out.println(name.get()); // Bob

// Optional로 값 조회
Optional<String> value = name.opt();
String result = value.orElse("default");

변경 이벤트 수신

Property<String> name = new ObjectProperty<>();

// 리스너 등록 — ChangeEvent로 이전 값과 새 값을 모두 수신
ListenerToken token = name.addListener(event -> {
    System.out.println("이전 값: " + event.getOldValue());
    System.out.println("새로운 값: " + event.getNewValue());
    System.out.println("변경 시각: " + event.getTimestamp());
});

name.set("Charlie"); // 리스너 호출됨

// 더 이상 필요 없을 때 토큰으로 리스너 제거
name.removeListener(token);

프로퍼티 동기화 (Entanglement)

Property<String> source = new ObjectProperty<>("Initial");
Property<String> target = new ObjectProperty<>();

// 양방향 동기화 설정 — source의 현재 값이 target으로 즉시 전파됨
source.entangle(target);
System.out.println(target.get()); // Initial

// source 변경 → target에 자동 전파
source.set("Updated");
System.out.println(target.get()); // Updated

// target 변경 → source에 자동 전파 (양방향)
target.set("Modified");
System.out.println(source.get()); // Modified

API 레퍼런스

Property<T> 인터페이스

읽기/쓰기가 모두 가능한 핵심 인터페이스입니다.

// 값 조회
T get()
Optional<T> opt()

// 값 설정
void set(T value)

// 리스너
ListenerToken addListener(Consumer<ChangeEvent<T>> listener)
ListenerToken addListener(Consumer<ChangeEvent<T>> listener, Predicate<T> predicate)
ListenerToken addWeakListener(Consumer<ChangeEvent<T>> listener)
boolean removeListener(ListenerToken token)
int getListenerCount()
void clearAllListeners()

// 동기화
void entangle(Property<T> other)
boolean unentangle(Property<T> other)
Set<Property<T>> getEntangled()
boolean isEntangled(Property<T> other)
void clearAllEntanglement()

// 값 검증
void setValidator(PropertyValidator<? super T> validator)

// 변경 추적
Optional<T> getPreviousValue()
boolean hasChanged()
int getChangeCount()

ReadOnlyProperty<T> 인터페이스

쓰기 권한 없이 읽기와 관찰만 허용하는 인터페이스입니다. Property<T>ReadOnlyProperty<T>를 확장합니다.

ObjectProperty<T>

Property<T>의 표준 구현체입니다. 값 저장, 리스너 관리, 동기화 로직 전체를 담당합니다.

ComputedProperty<T>

하나 이상의 소스 프로퍼티로부터 값을 자동 계산하는 파생 프로퍼티입니다. Property<T>를 구현하므로 다른 프로퍼티와 동기화(entangle)할 수 있습니다. 단, set()을 직접 호출하면 UnsupportedOperationException이 발생합니다.

// 단일 소스로부터 계산
ObjectProperty<String> name = new ObjectProperty<>("john");
ComputedProperty<String> upper = ComputedProperty.compute(name, String::toUpperCase);
System.out.println(upper.get()); // JOHN

// 두 소스를 결합
ObjectProperty<String> firstName = new ObjectProperty<>("John");
ObjectProperty<String> lastName  = new ObjectProperty<>("Doe");
ComputedProperty<String> fullName = ComputedProperty.combine(
    firstName, lastName,
    (f, l) -> f + " " + l
);
System.out.println(fullName.get()); // John Doe

// 세 소스를 결합 (Object[] 배열로 전달)
ObjectProperty<Integer> age = new ObjectProperty<>(30);
ComputedProperty<String> profile = ComputedProperty.combine(
    firstName, lastName, age,
    objs -> objs[0] + " " + objs[1] + " (age: " + objs[2] + ")"
);

// 소스 변경 시 자동 갱신
firstName.set("Jane");
System.out.println(fullName.get()); // Jane Doe

// 계산 결과를 다른 프로퍼티에 동기화
ObjectProperty<String> mirror = new ObjectProperty<>();
fullName.entangle(mirror);  // ComputedProperty → mirror 단방향 전파

ChangeEvent<T>

값 변경 시 리스너에게 전달되는 이벤트 객체입니다.

name.addListener(event -> {
    T old     = event.getOldValue();        // 이전 값
    T next    = event.getNewValue();        // 새로운 값
    long time = event.getTimestamp();       // 변경 시각 (ms)

    boolean changed     = event.hasChanged();       // old != new
    boolean initialized = event.isInitialized();    // null → non-null
    boolean cleared     = event.isCleared();        // non-null → null
});

ListenerToken

등록된 리스너를 고유하게 식별하는 토큰입니다. 리스너를 정확히 제거하거나 관리할 때 사용합니다.

ListenerToken token = property.addListener(event -> { /* ... */ });

long id        = token.getId();          // 고유 ID (나노초 기반)
long createdAt = token.getCreatedAt();   // 생성 시각 (ms)
long age       = token.getAge();         // 경과 시간 (ms)

property.removeListener(token);          // 토큰으로 리스너 제거

고급 사용법

조건부 리스너

Property<Integer> score = new ObjectProperty<>(0);

// 점수가 100 이상일 때만 리스너 호출
score.addListener(
    event -> System.out.println("만점 달성! " + event.getNewValue()),
    value -> value != null && value >= 100
);

score.set(80);  // 리스너 호출 안 됨
score.set(100); // "만점 달성! 100" 출력

약한 참조 리스너

Property<String> property = new ObjectProperty<>();

// 람다가 GC될 경우 자동으로 리스너에서 제거됨
property.addWeakListener(event ->
    System.out.println("변경됨: " + event.getNewValue())
);

값 검증

Property<String> email = new ObjectProperty<>();

// 단일 검증자
email.setValidator(PropertyValidator.notNull());

// 검증자 조합 — and() / or() / not()
PropertyValidator<String> validator = PropertyValidator.notNull()
    .and(PropertyValidator.minLength(5))
    .and(PropertyValidator.maxLength(100))
    .and(PropertyValidator.pattern("^[A-Za-z0-9+_.-]+@(.+)$"));
email.setValidator(validator);

email.set("valid@example.com");  // 정상
email.set("bad");                // IllegalArgumentException 발생

// 커스텀 검증자 (람다)
Property<Integer> age = new ObjectProperty<>();
age.setValidator(v -> v != null && v >= 0 && v <= 150);

내장 검증자 목록:

검증자 설명
PropertyValidator.accept() 모든 값 허용 (기본값)
PropertyValidator.notNull() null 거부
PropertyValidator.minLength(n) 최소 문자열 길이
PropertyValidator.maxLength(n) 최대 문자열 길이
PropertyValidator.pattern(regex) 정규식 패턴 매칭

조합 연산자:

연산자 설명
.and(other) 두 검증자 모두 통과해야 유효
.or(other) 둘 중 하나만 통과해도 유효
.not() 검증 결과 반전

변경 추적

Property<String> prop = new ObjectProperty<>("initial");

prop.set("updated");

System.out.println(prop.getPreviousValue().get()); // "initial"
System.out.println(prop.getChangeCount());         // 1
System.out.println(prop.hasChanged());             // true

체인된 계산 프로퍼티

ObjectProperty<String> raw = new ObjectProperty<>("  hello world  ");

ComputedProperty<String> trimmed  = ComputedProperty.compute(raw, String::trim);
ComputedProperty<String> upper    = ComputedProperty.compute(trimmed, String::toUpperCase);
ComputedProperty<String> labeled  = ComputedProperty.compute(upper, s -> "Result: " + s);

System.out.println(labeled.get()); // Result: HELLO WORLD

raw.set("  java  ");
System.out.println(labeled.get()); // Result: JAVA

여러 프로퍼티 동기화

Property<String> master = new ObjectProperty<>();
Property<String> replica1 = new ObjectProperty<>();
Property<String> replica2 = new ObjectProperty<>();

master.entangle(replica1);
master.entangle(replica2);

master.set("value");
// replica1, replica2 모두 "value"로 업데이트됨

동기화 해제

prop1.entangle(prop2);

prop1.unentangle(prop2);         // 특정 동기화 해제
prop1.clearAllEntanglement();    // 모든 동기화 해제

리스너 예외 격리

Property<String> property = new ObjectProperty<>();

// 리스너 A에서 예외가 발생해도 리스너 B는 계속 실행됨
property.addListener(event -> { throw new RuntimeException("오류 발생"); });
property.addListener(event -> System.out.println("정상 실행: " + event.getNewValue()));

property.set("value");
// 출력: "정상 실행: value"  (예외는 로깅만 됨)

아키텍처

클래스 계층 구조

ReadOnlyProperty<T>  (interface)
│   get(), opt()
│   addListener(), removeListener()
│   getPreviousValue(), hasChanged(), getChangeCount()
│   getValidator(), clearAllListeners()
│
└── Property<T>  (interface)
    │   set(), entangle(), unentangle()
    │   getEntangled(), isEntangled()
    │   setValidator(), clearAllEntanglement()
    │
    ├── ObjectProperty<T>  (final class)
    │       값 저장, 리스너 관리, 동기화 로직 전체 구현
    │       CopyOnWriteArrayList, ConcurrentLinkedQueue 기반
    │       ThreadLocal로 순환 참조 감지
    │
    └── ComputedProperty<T>  (final class)
            소스 프로퍼티로부터 값을 자동 계산
            ObjectProperty<T>에 위임(delegate) 패턴
            set() → UnsupportedOperationException

ObjectProperty 내부 구조

┌──────────────────────────────────────────┐
│  ObjectProperty<T>                       │
├──────────────────────────────────────────┤
│  value: volatile T                       │
│  previousValue: volatile T               │
│  validator: volatile PropertyValidator   │
│  changeCount: AtomicInteger              │
├──────────────────────────────────────────┤
│  listeners: CopyOnWriteArrayList         │  ← 스레드 안전 스냅샷
│  callbackQueue: ConcurrentLinkedQueue    │  ← 순차 실행 보장
│  executing: volatile boolean             │  ← 재진입 방지
├──────────────────────────────────────────┤
│  entangledWith: synchronized Set         │  ← 동기화 대상 집합
│  entangleVisited: ThreadLocal<Set>       │  ← 순환 참조 방지
└──────────────────────────────────────────┘

값 변경 흐름

set(newValue)
  │
  ├─ validator.isValid(newValue)  →  실패 시 IllegalArgumentException
  │
  ├─ 동일값 체크  →  동일하면 리스너 미호출
  │
  ├─ callbackQueue.clear()        ←  이전 미실행 콜백 제거
  │
  ├─ notifyListeners(ChangeEvent)
  │    └─ 각 ListenerEntry → callbackQueue에 추가
  │         └─ executeCallbacks() → 순차 실행
  │
  └─ propagateToEntangled(newValue)
       └─ ThreadLocal visited로 순환 참조 방지

빌드 및 테스트

# 빌드
./gradlew build

# 테스트
./gradlew test

# 클린 후 빌드
./gradlew clean build

# Javadoc 생성
./gradlew javadoc

# 로컬 Maven 저장소에 배포
./gradlew publish

프로젝트 구조

entanglement/
├── entangled/
│   ├── src/
│   │   ├── main/java/com/elex_project/entangled/
│   │   │   ├── ReadOnlyProperty.java      # 읽기 전용 인터페이스
│   │   │   ├── Property.java              # 핵심 읽기/쓰기 인터페이스
│   │   │   ├── ObjectProperty.java        # 메인 구현체
│   │   │   ├── ComputedProperty.java      # 파생 계산 프로퍼티
│   │   │   ├── ChangeEvent.java           # 변경 이벤트 객체
│   │   │   ├── ListenerToken.java         # 리스너 토큰
│   │   │   ├── PropertyValidator.java     # 값 검증 인터페이스
│   │   │   └── package-info.java          # 패키지 Javadoc
│   │   └── test/java/com/elex_project/entangled/
│   │       ├── ObjectPropertyTest.java         # ObjectProperty 테스트
│   │       ├── ComputedPropertyTest.java       # ComputedProperty 테스트
│   │       └── PropertyValidatorTest.java      # 검증자 테스트
│   └── build.gradle.kts
├── gradle/
├── gradlew
├── settings.gradle.kts
├── README.md
└── LICENSE

테스트 커버리지

64개 테스트 케이스, 실패/오류/스킵 없음.

ObjectPropertyTest (40개)

테스트 그룹 내용
기본 get/set 초기값, 값 설정, 동일값 중복 설정
ChangeEvent 이전/새 값, hasChanged(), isInitialized(), isCleared()
ListenerToken 토큰 고유성, 토큰으로 리스너 제거
조건부 리스너 Predicate 기반 선택적 발동
약한 참조 리스너 WeakReference 기반 자동 정리
값 검증 notNull, maxLength, 조합 검증자, pattern
변경 추적 이전 값, 변경 횟수, hasChanged
동기화 양방향, 다중, 해제, 전체 해제
순환 참조 방지 A→B→C→A 순환 감지
리스너 관리 리스너 수, 토큰 목록, 전체 제거
오류 처리 리스너 예외 격리, 유효하지 않은 값
순차 실행 등록 순서대로 리스너 호출 보장

ComputedPropertyTest (18개)

테스트 그룹 내용
단일 소스 초기 계산, 소스 변경 시 자동 갱신, ChangeEvent 발동
다중 소스 2-소스 결합, 첫/두 번째 소스 변경, 3-소스 결합
특성 테스트 set() 예외, getSources(), 리스너 지원, entangle(), 오류 처리, 동일값, 이전 값, 변경 횟수, 리스너 제거
복잡한 시나리오 체인 계산, 다중 계산 프로퍼티, 동기화 결합, 동기화 해제, 연쇄 동기화

PropertyValidatorTest (6개)

테스트 그룹 내용
기본 검증자 accept, notNull, minLength, maxLength, pattern
검증자 조합 and(), or(), not(), 복잡한 조합
통합 테스트 프로퍼티 적용, 복합 검증, 이메일/숫자 검증
Integer 검증 범위 검증 람다

설계 원칙

  • Java 21+: 최신 Java 기능 활용 (Records, Sealed, 패턴 매칭 등 검토)
  • Lombok: @Slf4j, @Getter, @ToString 등 보일러플레이트 최소화
  • SLF4J: 유연한 로깅 — error/warn 메시지는 영문, 그 외는 한국어
  • JUnit Jupiter: 경계 조건 포함 포괄적 테스트, Nested 클래스 구조
  • Javadoc: 모든 공개 API에 영문 완전 문서화
  • 불변성: 가능한 모든 필드·파라미터에 final 적용
  • 예외: 명확한 예외 타입 선택, 영문 상세 메시지 포함
  • 스레드 안전: CopyOnWriteArrayList, volatile, AtomicInteger, ThreadLocal 활용
  • Delegate 패턴: ComputedProperty는 내부적으로 ObjectProperty에 위임

의존성

의존성 용도
Java 21+ 언어 런타임
SLF4J 로깅 추상화
Lombok 코드 생성 (compileOnly)
Logback 로깅 구현체 (testImplementation)
JUnit Jupiter 테스트 프레임워크 (testImplementation)

라이선스

이 프로젝트는 Apache License 2.0으로 배포됩니다 — LICENSE 파일을 참고하세요.


기여

기여를 환영합니다. 이슈 및 풀 리퀘스트를 통해 참여해 주세요.

  1. 모든 테스트가 통과해야 합니다
  2. 새 기능에는 경계 조건을 포함한 포괄적인 테스트가 필요합니다
  3. 코드 스타일 및 작성 가이드라인을 준수해 주세요
  4. 공개 API에는 반드시 영문 Javadoc을 작성해 주세요

Roadmap

  • ObservableList<T>, ObservableMap<K,V> 등 컬렉션 타입 지원 -> ListProperty, MapProperty
  • JavaFX Property 인터페이스와의 호환 어댑터
  • Reactive Streams (Publisher/Subscriber) 통합
  • 대형 동기화 네트워크에서의 성능 최적화
  • Spring/Micronaut Bean과의 통합 지원
  • @Entangled 어노테이션 기반 선언적 동기화

Made with ❤️ by Elex Project
Elex Co., Pte. — https://www.elex-project.com/