프로그래밍 패러다임
* 프로그래밍 패러다임 = 프로그램을 구성하고 구현하는 사상이나 접근법
1. 명령형 프로그래밍(Imperative Programming)
: 프로그램이 어떻게 동작해야 하는지 세세한 제어 흐름을 통해 기술
: 절차지향, 객체지향 포함
: 변수의 값이 바뀌며 상태(state)가 변해감
: CPU 동작 방식과 유사하며 전통적 HW와 직관적인 일치
: ex) C, C++, Java 등
(+) 컴퓨터 동작 방식과 유사해 이해하기가 직관적, 제어 흐름을 상세히 제어하기 쉬움
(-) 프로그램 규모가 커지면 상태 변경에 따른 복잡도 증가
1.1 절차지향 : 프로시저, 함수 기반으로 로직을 절차적으로 구성
: 공통 로직을 재사용하기 위해 함수나 프로시저를 만들어 사용
> 함수 : 입력값을 받아 처리하고 결과값을 반환하는 것이 주 목적
> 프로시저 : 일련의 명령문들을 하나의 단위로 묶은것 -> 특정 작업이나 행동을 수행하는데 중점, 주로 상태 변경이나 특정 동작 수행에 초점
: 데이터와 절차가 분리되어 있다는 말로 자주 설명됨
: ex) C, Pascal 등
(+) 구조적 프로그래밍 기법(모듈화, 함수화)으로 코드 가독성 및 재사용성 증가
(-) 데이터와 로직이 명확히 분리되지 않을때, 유지보수가 어렵고 대형 플젝에서 복잡성 증가
1.2 객체지향 : 데이터, 함수를 하나로 묶은 객체를 중심으로 설계
: 캡슐화, 상속, 추상화, 다형성 등
: 데이터와 데이터를 처리하는 함수를 하나의 객체로 묶어 유지보수성, 확장성을 높임
: ex) Java, C++, C#
(+) 객체 단위로 묶여 대규모 시스템 설계에 적합
(-) 과도한 객체 분리나 복잡한 상속 구조로 복잡도가 오히려 증가할 수 있음
2. 선언적 프로그래밍(Declarative Programming)
: 무엇을 할 것인가를 기술하고, 어떻게 구현, 실행될지는 위임하는 방식
: 구체적인 제어 흐름을 직접 작성하기보다 원하는 결과나 조건을 선언적으로 표현
: 상태 변화보다 결과에 초점을 맞춰 코드 작성
: ex) SQL, HTML
(+) 구현의 복잡한 로직을 많이 숨길 수 있어 높은 수준에서 문제 해결에 집중 가능, 비지니스 로직을 직관적으로 표현하기 쉬움
(-) 언어나 환경이 제공하는 추상화 수준에 의존적이며, 내부 동작이 보이지 않을 경우 디버깅이 어려울 수 있음
3. 함수형 프로그래밍(Functional Programming)
: 무엇을 할 것인지를 수학적 함수로 구성하고 부수 효과 최소화 및 불변성을 강조하는 프로그래밍 방식
: 선언형 접근에 가까움 -> 어떻게가 아닌 어떤 결과를 원한다고 선언
: 순수 함수 중시 -> 같은 입력이 주어지면 항상 같은 출력
: 데이터는 불변 처리 -> 재할당 대신 새 데이터를 만들어 반환
: ex) Heskell, Clojure, Scala, Java(람다, 함수형 인터페이스를 통한 부분 지원)
(+) 상태 변화가 없거나 최소화되므로 디버깅 및 테스트 용이, 병렬 처리 및 동시성 처리가 간단
(-) 초기 접근이 어려울 수 있음, 계산 과정에서 메모리 사용이 증가할 수 있음
=> 이러한 패러다임은 서로 대립되지 않으며 문제 상황이나 설계 목표에 따라 적절히 선택하고 조합해 사용할 수 있다.
함수평 프로그래밍이란?
1. 순수 함수
> 같은 인자를 주면 항상 같은 결과를 반환하는 함수
2. 부수 효과 최소화
> 함수형 프로그래밍에서는 상태 변화를 최소화하기 위해 변수나 객체를 변경하는 것 지양
> 이로 인해 버그가 줄어들고, 테스트나 병렬 처리, 동시성 지원이 용이해짐
3. 불변성 지향
> 데이터는 생성된 후 가능한 한 변경하지 않고, 변경이 필요한 경우 새로운 값을 생성해 사용
> 프로그램 예측 가능성을 높여줌
4. 일급 시민 함수
> 함수가 일반 값(숫자, 문자열, 객체, 자료구조 등)과 동일한 지위를 가짐
> 함수를 변수에 대입하거나, 다른 함수의 인자로 전달하거나, 함수에서 함수를 반환하는 고차 함수
5. 선언형 접근
> 어떻게가 아닌 무엇을 계산할지 기술
> 가독성 높은 코드 작성
6. 함수 합성
> 간단한 함수를 조합해 더 복잡한 함수를 만드는 것 권장
6. 지연 평가
> 필요한 시점까지 계산을 미뤄 불필요한 계산 비용을 줄임
주요 함수형 프로그래밍 언어 및 활용
대표적인 순수 함수형 프로그램이 언어는 Haskell
Java, JS, Python등 전통적 언어들은 순수 함수형 프로그래밍 언어는 아니지만 람다 표현식, 고차 함수 등 함수형 스타일을 점진적으로 지원함
자바는 명령형, 객체지향이 주된 패러다임, 거기에 람다 등 함수형 문접이 일부 도입된 멀티 패러다임 언어!
자바와 함수형 프로그래밍
1. 순수 함수(Pure Function)
package functional;
import java.util.function.Function;
public class PureFunctionMain1 {
public static void main(String[] args) {
Function<Integer, Integer> function = x -> x * 2;
System.out.println(function.apply(10)); // 10 입력시 항상 20 반환 -> 순수 함수!
}
}
-> 입력값이 동일하면 항상 동일한 결과 반환 = 순수 함수
2. 부수 효과(Side Effect)
package functional;
import java.util.function.Function;
public class SideEffectMain1 {
public static int count = 0;
public static void main(String[] args) {
System.out.println("before count = " + count); // 0
Function<Integer, Integer> func = x -> {
count++; // 전역 변수 변경 -> 부수 효과
return x * 2;
};
func.apply(10);
System.out.println("after count = " + count); // 1
}
}
-> 함수 호출 전후로 count가 바뀌는데 이런 외부 상태 변화가 부수 효과의 대표적 예
-> 함수형 프로그램이에서는 이러한 외부 상태 변경을 최소화하는 것을 권장
3. 불변성 지향(Immutable State)
MutablePerson.java
package functional;
public class MutablePerson {
private String name;
private int age;
public MutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "MutablePerson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
-> setter로 객체 생성 후에도 상태값 변경 가능
ImmutablePerson.java
package functional;
public class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
// 변경이 필요한 경우 기존 객체를 수정하지 않고 새 객체 반환
public ImmutablePerson withAge(int newAge) {
return new ImmutablePerson(name, newAge);
}
@Override
public String toString() {
return "ImmutablePerson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
-> 필드가 final이며 생성 후에는 상태를 변경할 수 없음
-> 나이를 바꿀 때도 withAge로 새 객체를 생성하므로 원본 객체는 변하지 않고 부수효과가 최소화됨
ImmutableMain1.java
package functional;
public class ImmutableMain1 {
public static void main(String[] args) {
MutablePerson m1 = new MutablePerson("Kim", 10);
MutablePerson m2 = m1;
m2.setAge(11);
System.out.println(m1);
System.out.println(m2);
ImmutablePerson i1 = new ImmutablePerson("Kim", 10);
ImmutablePerson i2 = i1.withAge(21);
System.out.println(i1);
System.out.println(i2);
}
}
실행 결과

-> m1, m2가 동일 객체를 참조하므로 하나를 변경하면 원본도 변화함
-> ImmutablePerson은 withAge 호출 시 새 객체를 생성하기에 원본이 유지됨
4. 일급 시민 함수(First-class Citizen)
package functional;
import java.util.function.Function;
public class FirstClassCitizenMain {
public static void main(String[] args) {
// 함수를 변수에 담음
Function<Integer, Integer> func = x -> x * 2;
// 함수를 인자로 전달
applyFunction(10, func);
// 함수를 반환
getFunc().apply(10);
}
private static Function<Integer, Integer> getFunc() {
return x -> x * 2;
}
private static Integer applyFunction(int i, Function<Integer, Integer> func) {
return func.apply(i);
}
}
-> 함수를 변수처럼 취급
5. 선언형 접근
package functional;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class DeclarativeMain {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 명령형: for문과 조건 검사로 처리
List<Integer> result1 = new ArrayList<>();
for(int number : numbers) {
if(number % 2 == 0) { // 짝수인지 확인
result1.add(number * number); // 제곱한 값을 추가
}
}
System.out.println("Imperative Result: " + result1);
// 선언형: 스트림 API로 처리
Stream<Integer> result2 = numbers.stream()
.filter(number -> number % 2 == 0)
.map(number -> number * number);
System.out.println("Declarative Result: " + result2);
}
}
-> 명령형 방식은 어떻게 처리할지를 구체적으로 작성
-> 선언형 방식은 스트림의 filter, map 등을 조합해 무엇을 할지에 집중
7. 함수 합성(Composition)
package functional;
import java.util.function.Function;
public class CompositionMain {
public static void main(String[] args) {
// 1. x를 입력받아 x * x 하는 함수
Function<Integer, Integer> square = x -> x * x;
// 2. x를 입력받아 x + 1 하는 함수
Function<Integer, Integer> add = x -> x + 1;
// 함수 합성
// 1. compose()를 사용해 새로운 함수 생성
// square(add(2)) -> (2+1) * (2+1) = 9
Function<Integer, Integer> newFunc1 = square.compose(add);
System.out.println(newFunc1.apply(2)); // 9
// 2. andThen()을 사용해 새로운 함수 생성
// 먼저 square 적용 후 add를 적용하는 새로운 함수 생성
Function<Integer, Integer> newFunc2 = square.andThen(add);
System.out.println(newFunc2.apply(2)); // 5
}
}
-> compose는 뒤쪽 함수에서 앞쪽 함수 순으로 적용
-> andThen은 앞쪽 함수에서 뒤쪽 함수 순으로 적용
'인프런 > 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍' 카테고리의 다른 글
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 12. 병렬 스트림 (0) | 2025.07.03 |
|---|---|
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 11. 디폴트 메서드 (0) | 2025.06.30 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 10. Optional (1) | 2025.06.25 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 9. 스트림 API 3 - 컬렉터 (0) | 2025.06.23 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 9. 스트림 API 2 - 기능 (0) | 2025.06.17 |