디자인패턴에 관심이 있는 개발자라면 인터페이스의 중요성은 잘 알고 있을 것이다. 자바 8 이후로는 인터페이스의 하위 호환성을 지킬 수 있게 되었다.
- 자바 8 이전에는 하위 호환성을 지키면서 인터페이스에 메서드를 추가하는 것이 불가능했다.
- 자바 8 이후, default method가 추가되면서 새로운 인터페이스를 구현하지 않아 발생하는 컴파일 에러를 잡을 수 있게 되었다.
하지만, default 메서드에도 위험이 따른다. 아래 메서드를 살펴보자. java.util.Collection
의 removeIf 디폴트 메서드다.
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
자바 1.8 이후로 적용된 removeIf 메서드가 정말로 하위 호환성을 지켜줄까? apache commons-collections의 4.2 버전의 구현체를 보자. 이 라이브러리는 removeIf 메서드가 없다. 즉, 자바 1.8 이전에 만들어진 라이브러리다.
테스트를 위해 build.gradle에 아래와 같이 디펜던시를 추가하자.
dependencies {
compile group: 'org.apache.commons', name: 'commons-collections4', version: '4.2'
compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.30'
testCompile group: 'org.assertj', name: 'assertj-core', version: '3.19.0'
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.0'
}
다음과 같이 테스트 코드를 작성하자.
싱글 스레드 기반으로 SynchronizedCollection 라이브러리가 자바 1.8에서 컴파일에 성공함을 알 수 있다.
public class SingleThreadSynchronizedTest {
private static final Logger log = LoggerFactory.getLogger(SingleThreadSynchronizedTest.class);
@Test
void testSingleThread() {
List<String> list = new ArrayList<>();
list.add("스위프트");
list.add("자바");
list.add("스터디");
SynchronizedCollection<String> sc = SynchronizedCollection.synchronizedCollection(list);
String a = "스터디";
sc.forEach((s) -> {
log.info("before={}", s);
});
// "스터디"만 리스트에서 지울것이다.
sc.removeIf(p -> p.equals(a)); // 아파치 라이브러리에는 존재하지 않지만 자바 1.8의 디폴트 메서드로 구현됨
sc.forEach((s)-> {
log.info("after={}", s);
});
}
}
실행 결과, 결과에는 이상이 없다.
16 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - before=스위프트
16 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - before=자바
16 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - before=스터디
17 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - after=스위프트
17 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - after=자바
이번에는 멀티 스레드로 10개의 스레드를 생성하여 테스트해보자.
public class MultiThreadSynchronizedTest {
private static final AtomicInteger counter = new AtomicInteger(0);
private static final Logger log = LoggerFactory.getLogger(SingleThreadSynchronizedTest.class);
@Test
void testMultiThread() throws InterruptedException {
List<String> list = new ArrayList<>();
list.add("스위프트");
list.add("자바");
list.add("스터디");
list.add("삼터디");
list.add("사터디");
SynchronizedCollection<String> sc = SynchronizedCollection.synchronizedCollection(list);
String a = "스터디";
sc.forEach((s) -> {
log.info("before={}", s);
});
// 스레드 10개 생성
int nThread = 10;
ExecutorService executorService = Executors.newFixedThreadPool(nThread);
for (int i = 0; i < nThread; i++) {
executorService.submit(() -> {
int idx = counter.addAndGet(1);
log.info("idx = {} removeIf = {}", idx, sc.removeIf(p -> p.equals(a)));
return null;
});
}
executorService.awaitTermination(1, TimeUnit.SECONDS);
sc.forEach((s)-> {
log.info("after={}", s);
});
}
}
위 코드를 여러번 실행하면 결과가 제각각인 것을 확인할 수 있다. 즉 리스트 객체에 동기화 기능이 제대로 갖추어져 있지 않음을 알 수 있다.
15 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - before=스위프트
15 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - before=자바
15 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - before=스터디
16 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - before=삼터디
16 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - before=사터디
18 [pool-1-thread-3] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - idx = 3 removeIf = true
18 [pool-1-thread-4] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - idx = 4 removeIf = true
18 [pool-1-thread-2] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - idx = 2 removeIf = true
18 [pool-1-thread-1] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - idx = 1 removeIf = true
19 [pool-1-thread-5] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - idx = 5 removeIf = false
19 [pool-1-thread-7] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - idx = 7 removeIf = false
20 [pool-1-thread-6] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - idx = 6 removeIf = false
20 [pool-1-thread-8] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - idx = 8 removeIf = false
20 [pool-1-thread-9] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - idx = 9 removeIf = false
20 [pool-1-thread-10] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - idx = 10 removeIf = false
1026 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - after=스위프트
1026 [main] INFO me.cwpark.chapter4.item21.SingleThreadSynchronizedTest - after=자바
스위프트의 인터페이스는 프로토콜(protocol)로 불린다. 프로토콜에 구현 메서드를 추가하려면, extension을 통해 메서드 구현이 가능하다. 하지만 이 extension을 사용할 때 주의점이 있다.
protocol SampleProtocol {
func foo()
}
extension SampleProtocol {
func foo() {
print("protocol foo")
}
func bar() {
print("protocol bar")
}
}
class SampleClass: SampleProtocol {
func foo() {
print("class foo")
}
func bar() {
print("class bar")
}
}
let sample: SampleProtocol = SampleClass()
sample.foo() // "class foo"
sample.bar() // "protocol bar", "class bar" 가 출력되지 않는다!!!
SampleClass 클래스는 SampleProtocol이라는 프로토콜을 구현하고 있다. 하지만 SampleProtocol은 foo() 메서드만을 명세하고 있으므로, extension의 bar() 메서드가 구현체인 SampleClass의 메서드보다 우선한다.