들어가는 말
최근에 Java를 공부하면서, 평소에는 이해하지 못했던 이론이나 개념들이 이래서 사용하는 거구나! 라며 홀로 뮤레카를 외친 경험을 하였습니다. 오늘은 이에 대해서 3가지만 간략하게 정리해보는 시간을 가져볼까 합니다( 서로 다른 언어이기 때문에 알게된 점은 많지만 그 중 몸소 와닿은 3가지만 정리합니다. ).
프로그래밍 언어는 서로 다른 철학과 목적을 가질 수 있습니다. Java와 JavaScript는 이름이 비슷하지만, 그 특성과 사용 사례는 매우 다릅니다. 특히 디자인 패턴, 자료구조, 알고리즘의 개념을 접근하는 방식에서 차이를 느낄 수 있는데, JavaScript에서는 이러한 개념들의 필요성을 크게 느끼지 못하다가, Java에서의 활용을 보며 그 중요성을 실감하게 되었습니다.
디자인 패턴: 복잡한 객체 생성의 필요성
JavaScript에서는 객체를 동적으로 생성하는 것이 매우 쉬우며, 클래스 기반 설계가 엄격하지 않습니다(->근본이 프로토타입 기반의 객체지향). 하지만 Java에서는 복잡한 객체 생성이 필수적으로 요구되는 상황이 많으며, 이를 디자인 패턴을 통해 해결하는 방식이 자주 사용됩니다. 대표적으로 싱글톤 패턴과 빌더 패턴이 있습니다.
특히, 빌더 패턴은 스프링 부트를 학습하면서 심심치 않게 활용되는 것을 목격하였습니다. 처음에는 Builder().build() 이렇게 적힌 것이 뭐지 했는데, 디자인 패턴을 따로 학습하고 나니, 다양한 곳에서 명시적으로 활용되고 있다는 점을 알게 되었습니다.
싱글톤 패턴
JavaScript는 모듈 시스템을 통해 전역적으로 공유할 수 있는 변수를 다루기 쉽습니다. 하지만 Java는 멀티스레드 환경에서 안전하게 객체를 하나만 생성하기 위해 싱글톤 패턴과 멀티스레드 환경에서 발생할 수 있는 단점을 개선하기 위한 방식을 적극적으로 활용합니다.
Java
아래 코드에서는 null 체크를 두 번하고 synchornized 키워드를 사용하고 있습니다. 이는 다중 스레드가 동일한 인스턴스 생성에 관여하는 것을 피하도록 하는 이중잠금 방식으로 개선된 형태의 디자인 패턴을 적용한 것입니다.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
JavaScript의 대조
반면, JavaScript는 싱글스레드 이므로 멀티 스레드에서 발생하는 문제를 개선하기 위한 복잡한 싱글톤 패턴을 구현할 필요가 적으며, 클로저를 통해 유연하고 편리하게 구현됩니다(class 라는 문법적 설탕이 나오긴 했지만, 극명한 비교를 위해 모듈 패턴을 사용했습니다).
// JavaScript 싱글톤 패턴
const Singleton = (function() {
let instance;
function createInstance() {
const object = new Object("I am the instance");
return object;
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
이처럼 Java에서는 동기화와 같은 고급 기능을 사용해 단일 객체 생성을 보장하는 반면, JavaScript는 싱글스레드와 클로저를 기반으로 한 모듈 패턴을 활용해 간단하게 해결할 수 있습니다.
하지만, 다양하고 복잡한 작업을 처리할 수 있는 멀티스레드 환경에서 객체를 하나로 유지하는 데 있어 Java의 설계가 더 명시적이고, 명확하다는 것을 많이 느꼈습니다.
빌더 패턴
JavaScript에서는 객체 리터럴을 통해 쉽게 복잡한 구조의 객체를 생성할 수 있습니다. Java는 복잡한 객체를 만들 때 빌더 패턴을 사용하여 더 구조적인 접근을 합니다.
Java
Product 클래스 내부적으로 Builder 클래스는 static 으로 선언하고, 이를 내부 멤버 메소드처럼 접근하여 필수 멤버변수를 초기화하고 있습니다. build() 메소드가 최종적으로 실행되기 전 까지는 Builder 클래스 내부적으로 변경된 객체의 값들을 보유하고 있다가. build() 메소드를 호출하는 순간 Product 인스턴스를 호출하여 객체를 생성하는 아주 체계적인 방식으로 활용되고 있습니다.
public class Product {
private final String name;
private final double price;
private final String description;
private Product(Builder builder) {
this.name = builder.name;
this.price = builder.price;
this.description = builder.description;
}
public static class Builder {
private final String name;
private final double price;
private String description;
public Builder(String name, double price) {
this.name = name;
this.price = price;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Product build() {
return new Product(this);
}
}
}
🤔 Builder descripation 에서 return this 를 하는 이유
이는 Builder().description.build 와 같이 메소드 체이닝을 형성하기 위해서 입니다. 여기서 this 는 Builder 인스턴스 자체를 가리키기 때문에 가능한 방식입니다.
public Builder description(String description) {
this.description = description;
return this;
}
참고로 위 코드는 사용된다면 아래 예시와 같이 사용될 수 있습니다.
public class Main {
public static void main(String[] args) {
// 기본적인 Product 객체 생성
Product product1 = new Product.Builder("Laptop", 1200.00)
.description("High performance laptop")
.build();
// description을 생략하고 Product 객체 생성
Product product2 = new Product.Builder("Smartphone", 800.00)
.build();
// 객체 정보 출력
System.out.println("Product 1: " + product1);
System.out.println("Product 2: " + product2);
}
}
JavaScript의 대조
JavaScript에서는 간단하게 객체 리터럴을 사용하여 복잡한 객체를 만들 수 있습니다.
const product = {
name: "Laptop",
price: 1200.00,
description: "High performance laptop"
};
Java에서는 빌더 패턴을 통해 객체 생성 시 명확한 단계를 제공하는 반면, JavaScript는 객체 리터럴을 사용하여 덜 엄격한 방식으로 객체를 생성합니다.
그러나 Java 에서 사용된 빌더 패턴을 통해 복잡한 객체를 명확하게 정의할 수 있음을 알게 되었습니다. 무엇보다 해당 객체가 필요한 타이밍에 .build() 메소드를 호출하여 객체 리터럴을 선언하자마자 메모리 상에 등록되는 JavaScript 보다 메모리 사용에 대한 효율성도 높이고 있다는 점을 알 수 있었습니다.
다만, 빌더 패턴의 특성상 불변성을 가진 객체를 생성하는 것이기 때문에, 동적으로 속성을 추가하지 못합니다. 이는 JavaScript 와 크게 대조되는 특징이라 생각되었습니다.
자료구조: 명시적 사용의 차이
Java는 강력한 자료구조 라이브러리(ex. 컬렉션 프레임워크)를 제공하고, 대부분의 개발자들이 이를 적극적으로 활용합니다. 반면, JavaScript는 동적 자료형 특성으로 인해 별도의 자료구조를 사용하지 않고 객체나 배열로 많은 문제를 해결할 수 있습니다.
명확히 말하면 JavaScript의 객체나 배열도 내부적으로 복잡한 자료구조와 알고리즘이 적용된 결과이지만(ex. 배열은 내부적으로 해시 테이블을 사용), 명시적으로 보았을 때 기준으로 이야기 하였습니다. |
리스트 (List)와 배열 (Array)
Java는 List 인터페이스를 통해 다양한 리스트 구현체를 제공합니다. 예를 들어, ArrayList와 LinkedList는 각기 다른 성능 특성을 가지고 있어 상황에 맞게 선택할 수 있습니다.
Java
List<String> arrayList = new ArrayList<>();
List<String> linkedList = new LinkedList<>();
ArrayList는 인덱스 접근이 빠르지만, 삽입/삭제가 느립니다. ArrayList 는 동적 배열로서 JavaScript의 배열과 매우 유사합니다. 따라서 인덱스를 사용해서 해당 요소의 위치로 즉시 접근이 빠른 편입니다.
LinkedList는 노드와 노드 간에 참조 관계를 형성하는 자료구조로 삽입/삭제가 빠르지만, 순차적으로 검색하는 특성상 인덱스 접근이 느립니다.
JavaScript의 대조
JavaScript에서는 배열이 가장 많이 사용되며, 배열 내 삽입/삭제, 탐색 모두 지원하지만, Java처럼 성능 특성을 고려한 명시적 자료구조 선택은 드뭅니다.
let array = ["apple", "banana", "cherry"];
array.push("date"); // 끝에 추가
JavaScript의 배열은 동적이고, 링크드 리스트, 어레이 리스트 등과 같은 복잡한 자료구조를 따로 명시적으로 선택할 필요가 없습니다.
하지만 Java의 명시적인 자료구조 선택은 성능 최적화에 중요한 역할을 한다는 것을 알 수 있었습니다.
알고리즘: Java의 명시적 알고리즘 활용
JavaScript에서는 내장 메서드로 많은 알고리즘을 간단히 처리할 수 있지만, Java에서는 명시적으로 정렬 알고리즘이나 탐색 알고리즘을 작성하고 활용하는 것이 일반적입니다.
정렬 알고리즘
Java는 다양한 정렬 알고리즘을 명시적으로 구현하거나, Arrays.sort() 메서드를 사용해 정렬 알고리즘을 직접 관리할 수 있습니다.
Java
int[] arr = {5, 3, 8, 4, 2};
Arrays.sort(arr);
JavaScript의 대조
JavaScript에서는 sort() 메서드를 사용해 간단히 정렬을 할 수 있습니다.
let arr = [5, 3, 8, 4, 2];
arr.sort((a, b) => a - b);
Java에서는 알고리즘의 명시적 관리가 가능하며, 성능을 고려해 다양한 방법을 사용할 수 있음을 배웠습니다. JavaScript에서는 내부 알고리즘에 크게 신경 쓰지 않아도 되지만, Java에서는 이를 적극적으로 활용할 수 있습니다.
정리(나가는 말)
Java와 JavaScript는 디자인 패턴, 자료구조, 알고리즘에 대한 접근 방식에서 큰 차이를 보입니다. JavaScript에서는 이러한 개념들을 덜 명시적으로 다루는 경향이 있지만, Java에서 이를 명확하게 구현하고 관리하는 것을 보면서 그 중요성을 이해하게 되었습니다.
애초에 Java와 JavaScript는 이름만 비슷할 뿐 근본적으로 채택한 언어적 관점이 다르기 때문에, 이러한 비교가 불필요할지도 모릅니다. 다만, 최근에는 대신 TypeScript를 활용하고, 이는 정적 타입을 지원하여 백엔드 환경에서는 앞서 언급한 패턴을 적극적으로 활용하는 사례를 종종보았기 때문에, 한번 정리해보는 시간을 가져보았습니다.