- Java 100%
| .vscode | ||
| entangled | ||
| gradle | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| entanglement.png | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.md | ||
| settings.gradle.kts | ||
Property Entanglement - Spooky action at a distance
개요
Spooky action at a distance
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 파일을 참고하세요.
기여
기여를 환영합니다. 이슈 및 풀 리퀘스트를 통해 참여해 주세요.
- 모든 테스트가 통과해야 합니다
- 새 기능에는 경계 조건을 포함한 포괄적인 테스트가 필요합니다
- 코드 스타일 및 작성 가이드라인을 준수해 주세요
- 공개 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/
