객체지향 언어와 함수형 언어, 그리고 Java와 Kotlin의 관계
🌟 들어가기 전
내가 자주 쓰는 언어인 Java와 Kotlin.
두 언어는 모두 객체지향 언어이지만 Kotlin은 함수형 언어에 포함되기도 한다.
그렇다면 우리가 흔히 아는 객체지향 언어와 함수형 언어.
그들 사이의 차이는 어떤 것이고 특징은 어떨까?
🔍 세부 내용
✅ 객체지향 언어 (OOP)
객체지향 언어는 클래스와 객체를 기반으로 데이터와 메서드를 캡슐화하여 재사용성과 유지보수성을 높이는 특징을 가지고 있다.
가장 대표적인 특징 4가지 !
- 캡슐화(Encapsulation): 데이터와 메서드를 하나의 객체로 묶어 외부에 노출할 범위를 제어 (ex. getter/setter)
- 상속(Inheritance): 기존 클래스의 기능을 재사용하여 새로운 클래스를 생성
- 다형성(Polymorphism): 동일한 메서드 이름으로 다양한 동작을 수행
- 추상화(Abstraction): 핵심적인 데이터와 메서드만 노출하고 불필요한 세부 사항은 숨김
물론 위 4가지를 충족한다고 모두 객체지향 언어인 것은 아니지만,
객체와 클래스, 그리고 위 4가지의 특징을 가지고 있으면 대부분 객체지향 언어로 분류된다.
// 추상화
abstract class Animal {
private String name; // 캡슐화: 이름은 private으로 접근을 제한
public Animal(String name) {
this.name = name;
}
// 다형성: makeSound 메서드는 자식 클래스에서 구현해야 합니다.
public abstract void makeSound();
public String getName() {
return name;
}
}
// 상속: Dog는 Animal 클래스를 상속받아 구현
class Dog extends Animal {
public Dog(String name) {
super(name);
}
// 다형성: Dog만의 소리
@Override
public void makeSound() {
System.out.println(getName() + " says Woof!");
}
}
// 상속: Cat은 Animal 클래스를 상속받아 구현
class Cat extends Animal {
public Cat(String name) {
super(name);
}
// 다형성: Cat만의 소리
@Override
public void makeSound() {
System.out.println(getName() + " says Meow!");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog("Buddy");
Animal myCat = new Cat("Whiskers");
myDog.makeSound(); // Buddy says Woof!
myCat.makeSound(); // Whiskers says Meow!
}
}
가장 간단하게 객체지향 언어에 대한 이해를 도울 수 있는 예제 코드이다.
추상화, 다형성, 상속, 캡슐화 4가지의 특징을 모두 가지고 있다.
- 캡슐화: Animal 클래스의 name 변수는 private으로 설정되어 있어, getName()을 통해서만 접근 가능
- 상속: Dog과 Cat 클래스는 Animal 클래스를 상속
- 다형성: Dog과 Cat 객체에 따라 makeSound() 역할 구분
- 추상화: Animal 클래스에서 makeSound() 메서드를 추상 메서드로 정의 → Dog, Cat 클래스에서 구현
✅ 함수형 언어 (FP)
함수형 언어는 순수 함수와 불편 데이터를 기반으로 상태와 부작용을 최소화 하였다.
- 순수 함수(Pure Function): 동일 입력에 대해 항상 동일 출력, 부작용 없음.
- 고차 함수(Higher-Order Function): 함수를 인자로 전달하거나 반환 가능.
- 불변성(Immutability): 데이터 변경 없이 새로운 데이터 생성.
- 지연 계산(Lazy Evaluation): 필요할 때만 계산 수행.
// 1. 순수 함수 (Pure Function): 동일 입력에 대해 항상 동일 출력, 부작용 없음
fun add(a: Int, b: Int): Int {
return a + b
}
// 2. 고차 함수 (Higher-Order Function): 함수를 인자로 전달하거나 반환 가능
fun <T> List<T>.customFilter(predicate: (T) -> Boolean): List<T> {
return this.filter(predicate)
}
// 3. 불변성 (Immutability): 데이터 변경 없이 새로운 데이터 생성
data class Person(val name: String, val age: Int)
// 4. 지연 계산 (Lazy Evaluation): 필요할 때만 계산 수행
val lazyValue: String by lazy {
println("Calculating the lazy value...")
"Lazy Value"
}
fun main() {
// 1. 순수 함수 사용
println(add(3, 4)) // 항상 7을 반환, 동일한 입력에 대해 동일한 출력
// 2. 고차 함수 사용
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.customFilter { it % 2 == 0 }
println(evenNumbers) // [2, 4]
// 3. 불변성 사용
val person = Person("Alice", 25)
val olderPerson = person.copy(age = 26)
println(person) // Person(name=Alice, age=25)
println(olderPerson) // Person(name=Alice, age=26)
// 4. 지연 계산 사용
println(lazyValue) // 첫 호출 시 계산됨
println(lazyValue) // 두 번째 호출 시 계산 없이 값만 반환
}
✅ 객체지향 vs 함수형: 차이점 분석
특징 | 객체지향 스타일 | 함수형 스타일 |
코드 구조 | 명령형 (데이터 처리 절차를 명시) | 선언형 (데이터 흐름과 결과를 중심으로 작성) |
데이터와 함수의 관계 | 데이터와 함수를 캡슐화하여 객체로 관리 | 데이터를 전달하며 순수 함수로 처리 |
코드 간결성 | 로직이 비교적 명시적이나 장황할 수 있음 | 고차 함수로 간결하게 표현 가능 |
상태 변경 | 객체 상태 변경 가능 | 상태 변경 없이 새로운 데이터를 반환 |
병렬 처리 및 효율성 | 명령형 처리로 병렬화 어려움 | 함수형 스타일은 병렬 처리에 적합 |
✅ Java와 Kotlin에서의 객체지향 언어와 함수형 언어
같은 기능을 구현하되, Java로는 객체지향 언어로 구현을 하고 Kotlin으로는 함수형 언어로 구현을 해보았다.
import java.util.ArrayList;
import java.util.List;
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public int getScore() {
return score;
}
}
public class Main {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Alice", 85));
students.add(new Student("Bob", 92));
students.add(new Student("Charlie", 78));
int totalScore = 0;
for (Student student : students) {
totalScore += student.getScore();
}
double averageScore = totalScore / (double) students.size();
System.out.println("Average Score: " + averageScore);
}
}
객체지향 언어 특징을 살려 객체를 기반으로 Student 클래스를 모델링하였다.
그리고 for문을 통해 학생들의 score를 직접 계산하여 명령형으로 작성하였다.
data class Student(val name: String, val score: Int)
fun main() {
val students = listOf(
Student("Alice", 85),
Student("Bob", 92),
Student("Charlie", 78)
)
val averageScore = students
.map { it.score }
.average()
println("Average Score: $averageScore")
}
함수형 스타일에서는 map과 average의 고차 함수를 사용하여 데이터를 반환하도록 처리하였다.
명령형 스타일보다 선언형으로 바꾸었을 때, 훨씬 간결하게 작성이 된다.
🤔 배운 점 & 느낀 점
코딩테스트를 할 때 주 사용 언어를 Java에서 Kotlin으로 바꾸었다.
그러나 여전히 나는 객체지향 언어에 머물러 있다.
확실히 함수형 언어의 특징을 살려 선언형으로 소스 코드를 작성하면 훨씬 간결한 코드가 나온다.
물론 Kotlin은 객체지향 언어와 함수형 언어의 혼합 버전이기에 완전히 자유로울 수는 없지만
고차함수의 자유로운 활용에 대한 이점이 상당히 크다.
조금 더 선언형 언어에 익숙해지도록 공부해야겠다.