
자바스크립트로 프로그래밍을 처음 배웠을 때, 함수는 단순한 실행 단위가 아니라 변수에 담고, 다른 함수에 인자로 넘기고, 반환할 수도 있는 1급 객체였다. 이러한 개념은 자연스럽게 받아들여졌고, 다양한 고차 함수를 조합해 로직을 구성하는 방식에 익숙해졌다.
반면 자바는 객체지향 언어로서 함수보다는 클래스와 메서드를 중심으로 코드를 구성한다. 하지만 다행히도 자바 8부터는 함수형 프로그래밍 요소들이 도입되면서 표현력이 한층 풍부해졌다. 특히 람다 표현식, 함수형 인터페이스, 메서드 참조는 코드를 더욱 간결하고 유연하게 만들어준다. 여기에 더해 동작 파라미터화나 실행 어라운드 패턴 같은 구조적인 접근법은 실전 코드에서도 높은 재사용성과 응집력을 확보할 수 있게 해준다.
함수형 인터페이스
함수형 인터페이스(Functional Interface)는 단 하나의 추상 메서드만을 갖는 인터페이스를 말한다. 이 인터페이스는 람다 표현식 또는 메서드 참조로 구현될 수 있다. 자바 8에서 도입된 @FunctionalInterface 어노테이션을 사용하면, 컴파일 타임에 함수형 인터페이스 조건을 강제할 수 있다.
@FunctionalInterface
interface MyFunction {
void run();
}자바에서는 이미 다양한 기본 함수형 인터페이스를 제공한다. 대표적으로는 다음과 같다:
Runnable: () -> void — 아무 매개변수도 없이 실행
Function<T, R>: (T) -> R — 입력값 T를 받아서 R을 반환
Predicate
: (T) -> boolean — 조건 검사를 위한 인터페이스 Consumer
: (T) -> void — 입력값을 소비하고 아무것도 반환하지 않음 Supplier
: () -> T — 값을 공급함
이처럼 함수형 인터페이스는 람다 표현식이 "어디에 넣을 수 있는지"를 결정하는 중요한 기준이다.
람다 표현식
람다 표현식(Lambda Expression)은 익명 함수(anonymous function)를 작성하는 방법이다. 클래스나 메서드를 따로 정의하지 않고도, 필요한 동작을 바로 코드 블록으로 전달할 수 있게 해준다. 기본 문법은 다음과 같다:
(매개변수) -> { 실행문 }가령, Runnable 인터페이스를 구현한다고 했을 때, 익명 클래스를 사용하는 방식은 다음과 같다:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};하지만 람다를 사용하면 다음과 같이 간단하게 표현할 수 있다:
Runnable r = () -> System.out.println("Hello");람다 표현식은 함수형 인터페이스의 **함수 디스크립터(Function Descriptor)**와 일치하는 시그니처를 가져야 한다. 그래서 어떤 인터페이스에 람다를 넣을 수 있는지는 그 인터페이스의 추상 메서드 시그니처를 보면 알 수 있다.
함수 디스크립터는 함수형 인터페이스가 표현하는 "함수의 형태", 즉 매개변수 타입과 반환 타입으로 구성된 시그니처(signature)를 의미한다. 이것은 람다 표현식이 해당 인터페이스에 사용할 수 있는지를 판단하는 기준이 된다.
@FunctionalInterface
public interface Converter<F, T> {
T convert(F from);
}예를 들어 위와 같은 함수형 인터페이스가 있을 때, 이 인터페이스의 함수 디스크립터는 (F) -> T이다. 따라서 아래와 같이 (String) -> Integer 형태의 람다를 넣을 수 있다.
Converter<String, Integer> converter = s -> Integer.parseInt(s);이처럼 함수 디스크립터는 람다 표현식의 계약(contract) 역할을 하며, 어떤 인자와 어떤 반환값을 갖는지를 명확히 해야 한다.
메서드 참조
메서드 참조(Method Reference)는 이미 존재하는 메서드를 람다처럼 전달하는 문법이다. 람다 표현식이 단순히 어떤 메서드 하나만 호출하는 경우, 이를 메서드 참조로 대체할 수 있다. 메서드 참조는 가독성을 높이고, 람다 표현식을 더 직관적으로 표현할 수 있도록 도와준다. 문법은 다음과 같다:
[클래스이름 or 객체참조]::메서드이름정적 메서드 참조
Consumer<String> c = System.out::println;
// 람다로 쓰면: s -> System.out.println(s)특정 객체의 인스턴스 메서드 참조
String prefix = "Hello, ";
Function<String, String> f = prefix::concat;
// 람다로 쓰면: s -> prefix.concat(s)클래스의 인스턴스 메서드 참조
BiPredicate<String, String> p = String::equalsIgnoreCase;
// 람다로 쓰면: (a, b) -> a.equalsIgnoreCase(b)생성자 참조
Supplier<List<String>> s = ArrayList::new;
// 람다로 쓰면: () -> new ArrayList<String>()동작을 추상화하는 함수형 접근법
현대 소프트웨어 개발에서는 코드의 재사용성과 유연성을 높이기 위해 반복되는 로직을 일반화하고, 변하는 부분을 함수(또는 동작)로 분리하는 방법이 중요하다. 이때 동작을 추상화하여 함수나 객체로 전달하는 방식은 불필요한 중복을 줄이고, 다양한 상황에 맞게 코드를 쉽게 확장할 수 있는 기반이 된다.
자바 8부터 도입된 람다 표현식과 함수형 인터페이스는 이런 함수형 접근법을 자바 환경에서도 자연스럽게 구현할 수 있도록 돕는다. 특히, 동작 파라미터화(Behavior Parameterization)와 실행 어라운드 패턴(Execute Around Pattern)은 각각 다른 목적과 상황에서 이 ‘동작 추상화’ 개념을 효과적으로 활용하는 대표적인 사례다.
동작 파라미터화
동작 파라미터화(Behavior Parameterization)는 동작(behavior)을 인자로 전달하여, 코드의 로직 일부를 유연하게 변경하는 방식이다. 즉, 특정한 조건, 필터링, 정렬 등의 동작을 함수형 인터페이스를 통해 외부에서 주입하는 방식이다. 예를 들어 사과를 필터링하는 메서드를 생각해보자.
public interface ApplePredicate {
boolean test(Apple apple);
}
public List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}이제 람다로 다양한 조건을 전달할 수 있다:
List<Apple> greenApples = filterApples(inventory, a -> "green".equals(a.getColor()));
List<Apple> heavyApples = filterApples(inventory, a -> a.getWeight() > 150);이처럼 "무엇을 할 것인가"라는 동작을 메서드에 넘길 수 있게 되면, 코드 재사용성과 유연성이 매우 높아진다.
실행 어라운드 패턴
실행 어라운드 패턴(Execute Around Pattern)은 어떤 작업을 수행하기 위해 반복되는 준비 및 마무리 작업은 고정하고, 실제 수행할 동작만 외부에서 주입받는 구조를 말한다. 즉, 실행 어라운드 패턴을 사용하면 외부 리소스 관리 코드와 핵심 로직을 깔끔하게 분리할 수 있다. 특히 리소스를 열고 닫는 작업이 반복되는 I/O 처리, DB 연결, 트랜잭션 처리 등에서 유용하게 쓰인다.
예를 들어, 파일에서 한 줄을 읽는 작업을 보자:
public String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}여기서 파일을 열고 닫는 로직은 항상 동일하고, 실제로 읽는 로직만 다르다. 이 구조를 일반화해보자.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader br) throws IOException;
}
public String processFile(BufferedReaderProcessor processor) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return processor.process(br);
}
}이제 원하는 동작을 람다로 전달하여 실행할 수 있다:
String oneLine = processFile(br -> br.readLine());
String twoLines = processFile(br -> br.readLine() + br.readLine());더 읽어보기
2023.11.20
JDK
JDK는 자바 개발자용 도구 모음이다. 자바 소스 코드를 작성하고, 컴파일하고, 디버깅하며, 실행할 수 있는 모든 필수 도구가 포함되어 있다. 즉, 자바 프로그래밍을 시작하려면 반드시 JDK가 필요하다. 단순히 자바 프로그램을 실행하는 것과 개발하는 것은 다르다. 개발을 위해서는 소스…
2023.10.17
컬렉션 프레임워크
자바 컬렉션 프레임워크(Collection Framework)는 자바 프로그래밍에서 데이터를 효율적으로 저장하고 관리하기 위한 표준화된 자료구조와 알고리즘의 집합이다. 배열과 달리 크기 제한 없이 동적으로 데이터를 다룰 수 있고, 검색, 정렬, 삽입, 삭제와 같은 다양한 기능을 내장하고…
2023.09.24
클래스와 인터페이스
타입스크립트를 오랫동안 사용해오면서 클래스 기반 프로그래밍에 익숙해졌다고 생각했지만, 최근 Java를 배우기 시작하면서 기존에 알던 개념들을 더 깊이 있게 돌아보게 되었다. Java는 타입스크립트와 비교했을 때 문법적으로는 유사한 부분이 많지만, 객체지향 프로그래밍 언어로서의 철학과 제…
2026.06.07
AI 에이전트의 비밀값을 macOS Keychain에 맡기기
AI 에이전트나 스킬을 만들다보면 비밀값을 어떻게 관리하면 좋을지 하는 생각을 자주 하게 된다. API를 호출하려면 API Key가 필요하고, 특정 기능을 자동화하는 과정에서 아이디와 비밀번호가 필요할 수도 있다. 그런데 그 값을 프롬프트에 박아버리면 대화 기록에 남고, 명령어 인자로…
2026.06.01
React Server Components를 위한 컴포넌트 아키텍처
이 포스트는 Vercel의 Next.js 팀 소속 개발자 Aurora Scharff가 자신의 블로그에 올린 Component Architecture for React Server Components 게시글을 번역한 것이다. 번역하는 과정에서 다소 의역이 있을 수 있으며, 일부 번역에는…
2026.05.26
차트는 멈췄는데 윈도우가 움직인다
상황 어느날 서비스를 살펴보시던 팀장님께서 이런 말씀을 slack에 남기셨다. 진호님, 예측 차트에서 zoom을 계속하면 어느 순간 라인 차트가 아니라 단일 스캐터 차트처럼 보이는 데, 이거 수정하면 좋을 거 같아요. 어느정도 zoom을 하면 그 이후로는 zoom이 안 되도록 할 수 없…
2026.05.21
피자가게로 이해하는 디자인 패턴
에이든 피자는 처음부터 복잡한 시스템을 만들 생각이 없었다. 처음에는 메뉴 몇 개만 만들면 됐다. 그런데 손님은 커스텀 주문을 넣기 시작했고, 주방은 상태를 나눠야 했고, 결제와 배달앱과 알림이 하나씩 붙었다. 코드도 가게를 닮는다. 장사가 잘될수록 이상하게 더 쉽게 망가진다. 디자인…
댓글
댓글을 불러오는 중...