자바에서 람다식(lambda expression)은 함수형 프로그래밍을 지원하기 위해 도입된 기능으로,
코드를 간결하게 작성하고 가독성을 높이는 데 중요한 역할을 합니다.

이번 포스트에서는
매개변수가 없는 람다식, 매개변수가 있는 람다식, 리턴값이 있는 람다식을 중심으로 람다식의 개념과 활용법을 정리해 보겠습니다.

🔹 1. 매개변수가 없는 람다식

람다식은 함수형 인터페이스를 기반으로 동작합니다.
함수형 인터페이스란, "하나의 추상 메소드"만을 포함하는 인터페이스를 의미하며, 대표적인 예시로 Runnable이 있습니다.

람다식 기본 형태

@FunctionalInterface
public interface Workable {
    void work();  // 추상 메소드 (매개변수 없음)
}

이제, 위 인터페이스를 구현하는 방법을 보겠습니다.

익명 객체 방식 (전통적인 방식)

Workable worker = new Workable() {
    @Override
    public void work() {
        System.out.println("일을 합니다.");
    }
};
worker.work();  // 출력: 일을 합니다.

위와 같은 코드가 람다식을 사용하면 훨씬 간결해집니다.

람다식 사용 방식

Workable worker = () -> {
    System.out.println("일을 합니다.");
};
worker.work();  // 출력: 일을 합니다.

람다식에서는
매개변수가 없으면 ()를 사용하고,
실행문이 하나라면 {}를 생략할 수 있습니다.

Workable worker = () -> System.out.println("일을 합니다.");

✔ 실행문이 2개 이상일 경우 반드시 {}를 사용해야 합니다.


🔹 2. 매개변수가 있는 람다식

람다식에서 매개변수가 있는 경우, 메소드의 매개변수와 동일한 형태로 작성하면 됩니다.

람다식 기본 형태

@FunctionalInterface
public interface Speakable {
    void speak(String message);
}

이 인터페이스를 구현하는 방법을 보겠습니다.

익명 객체 방식

Speakable speaker = new Speakable() {
    @Override
    public void speak(String message) {
        System.out.println("말하기: " + message);
    }
};
speaker.speak("안녕하세요!");  // 출력: 말하기: 안녕하세요!

람다식 사용 방식

Speakable speaker = (message) -> {
    System.out.println("말하기: " + message);
};
speaker.speak("안녕하세요!");  // 출력: 말하기: 안녕하세요!

매개변수가 1개라면 ()를 생략할 수 있습니다.

 
Speakable speaker = message -> System.out.println("말하기: " + message);

매개변수가 2개 이상이면 반드시 ()를 사용해야 합니다.
✔ 실행문이 2개 이상이면 {}를 사용해야 합니다.


🔹 3. 리턴값이 있는 람다식

람다식에서 리턴값을 반환하는 경우, return 키워드를 활용합니다.

람다식 기본 형태

@FunctionalInterface
public interface Calculable {
    double calculate(double x, double y);
}

이제 위 인터페이스를 구현하는 다양한 방법을 보겠습니다.

익명 객체 방식

Calculable calculator = new Calculable() {
    @Override
    public double calculate(double x, double y) {
        return x + y;
    }
};
System.out.println(calculator.calculate(10, 5));  // 출력: 15.0

람다식 사용 방식

Calculable calculator = (x, y) -> {
    return x + y;
};
System.out.println(calculator.calculate(10, 5));  // 출력: 15.0

리턴문만 존재하는 경우 return과 {}를 생략할 수 있습니다.

Calculable calculator = (x, y) -> x + y;

메소드 참조를 활용하면 더욱 간결하게 작성할 수 있습니다.

Calculable calculator = Double::sum;
System.out.println(calculator.calculate(10, 5));  // 출력: 15.0

 

🔹 4. 람다식 활용 예제 (버튼 클릭 이벤트)

람다식은 GUI 프로그래밍에서 이벤트 처리에도 자주 사용됩니다.
예를 들어, onClickListener를 설정할 때 익명 객체를 사용하던 방식이 람다식으로 간단하게 변경될 수 있습니다.

익명 객체 방식

Button button = new Button();
button.setOnClickListener(new Button.ClickListener() {
    @Override
    public void onClick() {
        System.out.println("버튼이 클릭되었습니다.");
    }
});

람다식 사용 방식

button.setOnClickListener(() -> System.out.println("버튼이 클릭되었습니다."));

코드가 훨씬 간결해지며 가독성이 좋아집니다.
✔ 안드로이드 개발에서도 람다식이 매우 자주 사용됩니다.


🔹 5. 람다식의 장점과 활용

코드가 간결해진다 – 불필요한 익명 객체 코드를 줄이고 가독성을 높임.
함수형 프로그래밍 지원 – 스트림 API, 컬렉션 API와 결합하여 강력한 기능 제공.
이벤트 처리 간소화 – UI 프로그래밍에서 클릭 이벤트 등을 간단하게 처리 가능.
병렬 처리에 용이 – 멀티코어 환경에서 병렬 프로그래밍 활용 가능.
 
 
 
 
 
 

참조:
이것이 자바다 _ 신용권

 
 
 
 

 

🔥 람다식이란?

람다식(Lambda Expression)은 자바 8(Java 8)에서 도입된 기능으로,
익명 함수(Anonymous Function)를 보다 간결하게 표현할 수 있는 문법
함수형 프로그래밍(Functional Programming)을 지원
코드를 간결하게 작성할 수 있도록 도와주는 기법


🏗 1. 함수형 프로그래밍(Functional Programming)이란?

함수형 프로그래밍이란?

  • 프로그램을 함수(메소드) 단위로 작성하고, 데이터 처리를 함수에 맡기는 방식
  • 메소드(객체 소속)와 함수(독립적 실행 코드)의 차이
    • 메소드: 반드시 클래스 내부에 존재해야 함
    • 함수: 클래스와 무관하게 독립적으로 실행될 수 있음

📌 자바는 원래 객체지향 언어이지만, 함수형 프로그래밍을 지원하기 위해 람다식을 도입


🎯 2. 람다식의 기본 개념

람다식은 이름이 없는 함수(익명 함수)를 표현하는 방법입니다.

일반적으로 다음과 같은 형태로 작성됩니다.
📌 람다식 기본 문법

(매개변수) -> { 실행 코드 }

 
예제 1: 두 수를 더하는 람다식

(int x, int y) -> { return x + y; }

 
예제 2: 매개변수가 하나인 경우

x -> { return x * 2; }

 
예제 3: 실행 코드가 한 줄이면 중괄호 생략 가능

(x, y) -> x + y

 


🛠 3. 람다식과 익명 구현 객체의 관계

자바에서는 람다식을 익명 구현 객체(Anonymous Implementation Object)로 변환하여 실행합니다.
즉, 람다식은 사실상 익명 클래스를 줄여서 표현하는 방법입니다.
 
📌 익명 구현 객체 방식

interface Calculator {
    int calculate(int x, int y);
}

public class Main {
    public static void main(String[] args) {
        Calculator add = new Calculator() {
            @Override
            public int calculate(int x, int y) {
                return x + y;
            }
        };
        System.out.println(add.calculate(5, 3)); // 8
    }
}

 
위 코드를 람다식으로 변환

Calculator add = (x, y) -> x + y;
System.out.println(add.calculate(5, 3)); // 8

 
📌 결과는 동일하지만 코드가 훨씬 간결해짐! 🚀


🎯 4. 람다식을 사용하기 위한 조건

람다식은 함수형 인터페이스(Functional Interface)에서만 사용 가능합니다.
즉, 추상 메소드가 1개만 있는 인터페이스에서만 람다식을 사용할 수 있습니다.
 
📌 함수형 인터페이스(Functional Interface) 예제

@FunctionalInterface
interface Calculator {
    int calculate(int x, int y);
}

추상 메소드가 1개만 존재하므로 람다식 사용 가능!
만약 2개 이상의 추상 메소드가 존재하면 람다식 사용 불가능!


🏗 5. 람다식을 활용한 데이터 처리

람다식을 사용하면 데이터 처리 방식을 함수로 전달할 수 있음
즉, 함수를 변수처럼 사용 가능!
 
📌 람다식을 활용한 데이터 처리 예제

public class Main {
    public static void processNumbers(int a, int b, Calculator calculator) {
        int result = calculator.calculate(a, b);
        System.out.println("결과: " + result);
    }

    public static void main(String[] args) {
        processNumbers(5, 3, (x, y) -> x + y); // 더하기
        processNumbers(5, 3, (x, y) -> x - y); // 빼기
        processNumbers(5, 3, (x, y) -> x * y); // 곱하기
    }
}​
 

함수를 변수처럼 전달하여 유연하게 데이터 처리 가능!


🛠 6. 자바 표준 함수형 인터페이스

자바는 람다식을 쉽게 사용할 수 있도록 표준 함수형 인터페이스를 제공합니다.

📌 대표적인 함수형 인터페이스

인터페이스 추상 메소드 설명
Function<T, R>apply(T t)입력값을 받아 변환하여 반환
Consumer<T>accept(T t)입력값을 받아 처리 (반환값 없음)
Supplier<T>get()매개변수 없이 결과를 반환
Predicate<T>test(T t)조건식을 검사하여 true 또는 false 반환

 
예제: Function 인터페이스 활용

Function<String, Integer> lengthFunction = s -> s.length();
System.out.println(lengthFunction.apply("Hello")); // 5

 
예제: Consumer 인터페이스 활용

Consumer<String> printConsumer = s -> System.out.println(s);
printConsumer.accept("Hello, World!"); // Hello, World!​

 

 
예제: Predicate 인터페이스 활용

Predicate<Integer> isEven = num -> num % 2 == 0;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(5)); // false

 


🏁 정리: 람다식의 개념과 활용

📌 람다식(Lambda Expression)은 자바 8부터 도입된 기능으로, 익명 함수를 표현하는 방법
📌 함수형 인터페이스(Functional Interface)에서만 사용 가능 (추상 메소드 1개 필요)
📌 람다식을 사용하면 코드가 간결해지고, 데이터 처리 방식이 유연해짐
📌 자바에서 표준 함수형 인터페이스를 제공하여 람다식 활용을 쉽게 지원


이 블로그 포스트를 통해 배운 점

  • 람다식이 무엇이며, 왜 필요한지 이해했다.
  • 기존 익명 구현 객체 방식과 비교하여 코드가 간결해지는 것을 확인했다.
  • 자바의 표준 함수형 인터페이스를 활용하면 코드 재사용성이 높아진다는 점을 배웠다.
  • 람다식을 사용하여 데이터 처리 방식을 함수로 전달할 수 있음을 학습했다.

 
 
 
 
 

참조:
이것이 자바다 _ 신용권

 
 
 

MurmurHash3

1. MurmurHash3란?

MurmurHash3는 높은 성능과 뛰어난 해시 품질을 제공하는 비암호학적 해시 함수이다.

원래 Austin Appleby에 의해 개발되었으며, 특히 해시 테이블과 같은 데이터 구조에서 균등한 해시 분포를 제공하는 데 최적화되어 있다.

자바에서는 Apache Commons Codec 또는 Guava 라이브러리를 활용하여 MurmurHash3을 사용할 수 있다.


2. MurmurHash3의 특징

  • 비암호학적 해시 함수: 보안이 아닌 빠르고 균일한 해싱을 목표로 함
  • 우수한 해시 분포: 충돌 가능성이 낮고, 데이터 분포가 고르게 퍼짐
  • 빠른 성능: CPU 친화적인 설계로 매우 빠르게 해시 값을 생성
  • 고정된 크기의 해시 출력: 32비트 또는 128비트 해시 값 제공
  • 엔디언(Endianness) 독립적: x86과 x64 아키텍처에서 일관된 결과 보장

3. MurmurHash3 구현 방법

3.1. Apache Commons Codec을 이용한 MurmurHash3

Apache Commons Codec 라이브러리는 MurmurHash3을 쉽게 사용할 수 있도록 제공한다.

Maven 의존성 추가

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.15</version>
</dependency>

Java 코드 예제

import org.apache.commons.codec.digest.MurmurHash3;

public class MurmurHashExample {
    public static void main(String[] args) {
        String input = "Hello, MurmurHash3!";
        int hash32 = MurmurHash3.hash32x86(input.getBytes());
        long[] hash128 = MurmurHash3.hash128x64(input.getBytes());

        System.out.println("32-bit Hash: " + hash32);
        System.out.println("128-bit Hash: " + hash128[0] + ", " + hash128[1]);
    }
}

3.2. Guava 라이브러리를 이용한 MurmurHash3

Google Guava 라이브러리에도 MurmurHash3 기반 해시 기능이 포함되어 있다.

Maven 의존성 추가

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.0.1-jre</version>
</dependency>

Java 코드 예제

import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;

public class GuavaMurmurHashExample {
    public static void main(String[] args) {
        HashFunction murmur3_32 = Hashing.murmur3_32();
        int hashValue = murmur3_32.hashString("Hello, Guava MurmurHash3!", StandardCharsets.UTF_8).asInt();
        
        System.out.println("MurmurHash3 (32-bit) using Guava: " + hashValue);
    }
}

4. MurmurHash3의 다양한 활용 사례

활용 사례 설명
해시 테이블 균일한 해시 분포로 충돌을 최소화
Bloom Filter 빠른 해싱으로 효율적인 확률적 데이터 구조 구현
데이터 샘플링 대규모 데이터에서 균등한 샘플 추출
로그 분석 대량의 로그 데이터를 효과적으로 해싱하여 색인
게임 및 그래픽 무작위 값 생성, 셰이딩 기법 등에서 활용

5. MurmurHash3와 다른 해시 함수 비교

해시 함수 속도 충돌 가능성 암호학적 보안
MurmurHash3 빠름 매우 낮음 보안 목적 X
MD5 중간 높음 보안 취약
SHA-256 느림 극히 낮음 암호학적 안전
CRC32 매우 빠름 높음 보안 목적 X

MurmurHash3는 보안이 아닌 빠른 데이터 해싱과 균등한 분포가 필요한 경우에 최적화된 해시 함수이다.


6. 결론

MurmurHash3는 빠르고 균일한 해시 값을 생성하는 비암호학적 해시 함수로, 해시 테이블, Bloom Filter, 로그 분석 등의 다양한 분야에서 활용된다.
Java에서는 Apache Commons Codec이나 Google Guava 라이브러리를 통해 손쉽게 구현할 수 있으며, 성능과 균일한 해시 분포가 필요한 경우 최적의 선택이 될 수 있다.

자바 BitSet 클래스

1. BitSet이란?

BitSet은 자바에서 비트 배열을 다룰 수 있도록 제공하는 클래스이다.

boolean 값을 저장하는 boolean[] 배열보다 메모리를 효율적으로 사용할 수 있으며, 비트 연산을 활용하여 빠르게 데이터를 조작할 수 있다.

BitSet은 내부적으로 long[] 배열을 사용하여 비트를 저장하며, 필요에 따라 크기를 동적으로 조정할 수 있다.


2. BitSet의 주요 특징

  • 비트 단위로 저장: 0과 1로 이루어진 비트 배열을 저장 및 관리할 수 있음
  • 자동 크기 조정: 선언 시 크기를 지정하지 않아도 필요에 따라 크기가 자동으로 확장됨
  • 효율적인 메모리 사용: boolean[] 배열보다 메모리 사용량이 적음
  • 빠른 연산 속도: 비트 단위 연산 (AND, OR, XOR 등)을 지원하여 빠른 연산 가능
  • 0 기반 인덱스: 비트는 0부터 시작하는 인덱스로 접근 가능

3. BitSet 기본 사용법

3.1. BitSet 생성

import java.util.BitSet;

public class BitSetExample {
    public static void main(String[] args) {
        BitSet bitSet = new BitSet(); // 기본 크기로 BitSet 생성
        BitSet bitSetWithSize = new BitSet(10); // 초기 크기 10으로 생성
    }
}

3.2. 비트 설정 및 조회

BitSet bitSet = new BitSet();
bitSet.set(0); // 0번 비트를 1로 설정
bitSet.set(3); // 3번 비트를 1로 설정
bitSet.set(5, true); // 5번 비트를 1로 설정

System.out.println(bitSet); // 출력: {0, 3, 5}
System.out.println(bitSet.get(3)); // true (3번 비트는 1)
System.out.println(bitSet.get(4)); // false (4번 비트는 0)

3.3. 비트 해제 및 토글

bitSet.clear(3); // 3번 비트를 0으로 설정
bitSet.flip(5); // 5번 비트를 반전 (1 → 0 또는 0 → 1)
System.out.println(bitSet); // 출력: {0}

3.4. 논리 연산

BitSet bitSet1 = new BitSet();
bitSet1.set(0);
bitSet1.set(2);

BitSet bitSet2 = new BitSet();
bitSet2.set(1);
bitSet2.set(2);

bitSet1.and(bitSet2); // AND 연산 (둘 다 1인 경우만 유지)
System.out.println(bitSet1); // 출력: {2}

bitSet1.or(bitSet2); // OR 연산 (하나라도 1이면 유지)
System.out.println(bitSet1); // 출력: {0, 1, 2}

bitSet1.xor(bitSet2); // XOR 연산 (둘 다 1이면 0, 하나만 1이면 유지)
System.out.println(bitSet1); // 출력: {0, 1}

4. BitSet의 기타 유용한 메서드

메서드 설명
set(int index) 특정 비트를 1로 설정
clear(int index) 특정 비트를 0으로 설정
flip(int index) 특정 비트를 반전 (0↔1)
get(int index) 특정 비트의 값을 반환 (true/false)
cardinality() 1로 설정된 비트의 개수 반환
length() 가장 높은 1의 인덱스 + 1 반환
size() 내부적으로 사용되는 비트 배열 크기 반환
isEmpty() 모든 비트가 0인지 확인
toString() 1로 설정된 비트 목록을 문자열로 반환

 

예제:

BitSet bitSet = new BitSet();
bitSet.set(0);
bitSet.set(3);
bitSet.set(5);

System.out.println(bitSet.cardinality()); // 1로 설정된 비트 개수: 3
System.out.println(bitSet.length()); // 가장 높은 1의 인덱스 + 1: 6
System.out.println(bitSet.isEmpty()); // false (비트가 하나 이상 1임)
System.out.println(bitSet); // 출력: {0, 3, 5}

5. BitSet과 배열 비교

비교 항목 BitSet boolean 배열
메모리 효율성 높음 낮음 (각 요소가 1 byte)
크기 조정 자동 확장 고정 크기
논리 연산 지원 (AND, OR, XOR) 직접 구현해야 함
사용 용도 비트 플래그, 집합 연산 단순한 논리 값 저장

6. BitSet의 활용 예제

6.1. 중복 검사 (빠른 중복 체크)

int[] numbers = {1, 3, 5, 7, 3, 1};
BitSet seen = new BitSet();

for (int num : numbers) {
    if (seen.get(num)) {
        System.out.println("중복된 숫자: " + num);
    }
    seen.set(num);
}

6.2. 소수 판별 (에라토스테네스의 체)

int n = 50;
BitSet primes = new BitSet(n + 1);
primes.set(2, n + 1);

for (int i = 2; i * i <= n; i++) {
    if (primes.get(i)) {
        for (int j = i * i; j <= n; j += i) {
            primes.clear(j);
        }
    }
}
System.out.println("소수: " + primes);

7. 결론

BitSet은 메모리를 절약하면서도 빠른 비트 연산을 수행할 수 있도록 도와주는 강력한 도구이다.

boolean[] 배열보다 훨씬 효율적으로 데이터를 저장하고 연산할 수 있으며, 대량의 데이터를 다루는 경우 특히 유용하다.

데이터 중복 검사, 논리 연산, 집합 연산, 비트 플래그 등 다양한 활용이 가능하므로, 적절한 상황에서 BitSet을 적극 활용하면 성능을 크게 향상시킬 수 있다!

'Web Programming Language > JAVA' 카테고리의 다른 글

람다식(Lambda Expression)이란?  (0) 2025.03.05
MurmurHash3  (1) 2025.02.17
DTO와 VO  (0) 2025.02.12
Records  (1) 2025.02.12
orElse vs orElseGet 차이점  (0) 2025.02.12

DTO와 VO란?

DTO(Data Transfer Object)와 VO(Value Object)는 서로 다른 목적을 가진 객체이지만, 개발자들 사이에서 혼용되어 사용되는 경우가 많음.
이러한 혼동의 원인은 일부 서적에서 VO를 DTO로 잘못 정의한 데에서 비롯됨.


DTO (Data Transfer Object)

  • 데이터 전송을 위한 바구니 역할을 하는 객체.
  • 주로 컨트롤러 → 서비스 → DAO 등 계층 간 데이터 전달을 위해 사용됨.
  • Setter가 존재하면 가변 객체가 되지만, Setter 없이 생성자로 초기화하면 불변 객체로 만들 수 있음.
  • Entity와 DTO는 반드시 분리해야 함!
    • Entity는 DB와 직접 연결되는 핵심 클래스이므로 요청 및 응답 데이터 전달용으로 사용하면 안 됨.
    • DTO를 사용하면 뷰 변경이 Entity에 영향을 주지 않음.

DTO 예제

public class CrewDto {
    private final String name;
    private final String nickname;

    public CrewDto(String name, String nickname) {
        this.name = name;
        this.nickname = nickname;
    }

    public String getName() {
        return name;
    }

    public String getNickname() {
        return nickname;
    }
}
 

VO (Value Object)

  • 값 자체를 표현하는 객체이며, 속성 값이 같으면 같은 객체로 판단됨.
  • 불변(Immutable) 객체로 설계되어야 하며, Setter를 사용하지 않음.
  • 비즈니스 로직을 포함할 수 있음.
  • equals()와 hashCode()를 오버라이딩하여 값이 동일하면 같은 객체로 판단하도록 구현해야 함.

VO 예제

public class Money {
    private final int value;

    public Money(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return value == money.value;
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}

 

✅ 같은 금액을 가진 Money 객체들은 동일한 객체로 판단됨.


DTO vs VO 비교 정리

비교 항목 DTO (데이터 전달 객체) VO (값 객체)
목적 레이어 간 데이터 전달 값 자체를 표현
변경 가능 여부 가변 객체 가능 불변 객체
로직 포함 여부 Getter, Setter만 가능 추가 로직 포함 가능
비교 기준 객체 주소 비교 속성 값 비교 (equals, hashCode 오버라이딩)

 

📌 결론:

  • DTO는 데이터를 계층 간 전달하는 역할을 하므로 불필요한 로직을 포함해서는 안 됨.
  • VO는 값 자체를 표현하는 객체이므로 값이 같으면 같은 객체로 인식해야 함.

 

 


 

 

Entity란?

Entity는 데이터베이스의 테이블과 직접 매핑되는 클래스로,
도메인 모델에서 비즈니스 로직을 포함할 수 있는 핵심 객체이다.

 

Entity의 주요 특징

  • DB 테이블과 1:1 매핑되는 클래스
  • 비즈니스 로직을 포함할 수 있음
  • 고유 식별자(PK, ID)를 통해 객체를 구분
  • DB 변경을 반영해야 하는 경우 Entity를 수정해야 함
  • Setter를 최소화하고, 필요할 때만 값 변경 허용

Entity 예제

@Entity
public class User {
    @Id 
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String email;

    protected User() {} // JPA 기본 생성자

    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }

    // 비즈니스 로직 포함 가능
    public void changeEmail(String newEmail) {
        this.email = newEmail;
    }
}

 

📌 특징:

  • @Entity 어노테이션으로 DB 테이블과 매핑됨
  • @Id를 이용해 PK(고유 식별자)를 사용하여 비교
  • 비즈니스 로직을 포함할 수 있음
  • Setter 대신 변경 메서드(changeEmail())를 제공하여 데이터 변경 관리

DTO vs VO vs Entity 비교

구분 DTO (Data Transfer Object) VO (Value Object) Entity (엔티티)
목적 데이터 전달 (계층 간 통신) 값 자체를 표현 DB 테이블과 매핑
영역 컨트롤러 ↔ 서비스 ↔ DAO 도메인 모델 데이터베이스
변경 가능 여부 가변(Setter 존재 가능) 불변(Setter 없음) 가변 (Setter 최소화)
비교 기준 객체 주소(레퍼런스) 비교 속성 값 비교 (equals & hashCode 오버라이딩) ID(PK) 비교
로직 포함 여부 Getter/Setter만 포함 로직 포함 가능 비즈니스 로직 포함 가능
사용 사례 API 응답, 요청 객체 Money, Color 같은 값 표현 객체 User, Order 같은 도메인 모델

 

Entity, DTO, VO 활용 사례

사용 예시 DTO VO  Entity
DB 저장용
API 요청/응답 데이터
비즈니스 로직 포함
불변 객체 ❌ (Setter가 없으면 가능)
데이터 전송 용도

정리

DTO는 데이터를 안전하게 전달하는 역할을 하며, API 응답 및 요청에서 활용됨.
VO는 값 자체를 표현하는 불변 객체이며, equals()와 hashCode()를 오버라이딩해야 함.
Entity는 DB 테이블과 매핑되며 비즈니스 로직을 포함할 수 있음.
Entity와 DTO를 반드시 분리해야 하며, VO는 특정 값 표현에만 사용해야 함.

 

📌 결론:

  • Entity를 API 응답에 직접 사용하지 않고 DTO를 활용하자!
  • VO는 값 기반 비교가 필요할 때 사용하자!
  • DTO는 데이터를 전달하는 역할에 충실하자!

 

참조
https://www.youtube.com/watch?v=J_Dr6R0Ov8E
https://www.youtube.com/watch?v=z5fUkck_RZM

'Web Programming Language > JAVA' 카테고리의 다른 글

MurmurHash3  (1) 2025.02.17
BitSet 클래스  (0) 2025.02.17
Records  (1) 2025.02.12
orElse vs orElseGet 차이점  (0) 2025.02.12
JAVA) print, printf, println 차이점  (0) 2021.03.11

자바의 Records란?

Java의 Records(레코드) 는 Java 14에서 프리뷰 기능으로 처음 도입되었고, Java 16에서 정식 기능으로 추가됨.
레코드는 특정 데이터를 저장하기 위한 객체를 간단히 만들 수 있도록 설계된 새로운 유형의 클래스.
일반적으로 데이터 저장용으로만 사용하는 클래스에서 Boilerplate Code(반복되는 코드)를 줄여주는 것이 핵심 기능.


기존 방식 vs. Records 방식

기존 방식: 일반 클래스 사용

데이터를 저장하는 단순한 객체를 만들 때도 여러 가지 메서드와 필드를 정의해야 함.

예제: Employee 클래스 (기존 방식)

public class Employee {
    private final String name;
    private final int employeeNumber;

    // 생성자
    public Employee(String name, int employeeNumber) {
        this.name = name;
        this.employeeNumber = employeeNumber;
    }

    // Getter 메서드
    public String getName() {
        return name;
    }

    public int getEmployeeNumber() {
        return employeeNumber;
    }

    // toString() 메서드
    @Override
    public String toString() {
        return "Employee{name='" + name + "', employeeNumber=" + employeeNumber + "}";
    }

    // equals() & hashCode() 메서드
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return employeeNumber == employee.employeeNumber &&
               name.equals(employee.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, employeeNumber);
    }
}

 

문제점:

  • 단순한 데이터 저장용 클래스임에도 불구하고 50줄 이상의 코드가 필요.
  • toString(), equals(), hashCode() 등의 반복적인 코드(Boilerplate Code)가 많음.

새로운 방식: Java Records 사용

위와 같은 클래스를 한 줄의 코드로 정의 가능!

public record Employee(String name, int employeeNumber) {}

 

기본적으로 포함되는 기능들:

  1. private final 필드 자동 생성
  2. getter() 메서드 자동 생성 (get 접두사 없이 name(), employeeNumber() 형태)
  3. toString(), equals(), hashCode() 자동 생성
  4. Canonical Constructor(모든 필드를 초기화하는 생성자) 자동 제공

Records의 주요 특징

1. 불변 객체 (Immutable)

  • 레코드는 불변 객체 이므로, 생성된 후 필드 값을 변경할 수 없음.
  • 따라서 Setter 메서드가 자동으로 생성되지 않음.
Employee emp = new Employee("John", 12345);
emp.name = "Mike"; // 컴파일 에러 ❌

 

대신 새 객체를 생성해야 변경 가능:

Employee emp2 = new Employee("Mike", emp.employeeNumber());

2. 자동 생성되는 메서드

  • toString() 메서드:
Employee emp = new Employee("John", 12345);
System.out.println(emp);
// 출력: Employee[name=John, employeeNumber=12345]
  • equals() & hashCode():
Employee emp1 = new Employee("John", 12345);
Employee emp2 = new Employee("John", 12345);
System.out.println(emp1.equals(emp2)); // true ✅

3. Compact Constructor (압축 생성자)

  • 레코드는 기본적으로 Canonical Constructor(모든 필드를 초기화하는 생성자)를 자동 생성.
  • 하지만 특정 로직을 추가하려면 Compact Constructor 사용 가능.

 

✅ Compact Constructor란?

Compact Constructor는 record 내부에서 모든 필드를 대상으로 공통 검증 로직이나 전처리 로직을 넣고 싶을 때 사용하는 생성자입니다.

 

📌 특징

항목 설명
생성자 선언 시 파라미터 생략 record에 선언된 필드와 동일한 순서와 타입으로 자동 인식
필드 초기화 생략 가능 this.name = name 같은 코드는 자동 삽입됨
용도 유효성 검사, 형 변환, 예외 처리 등
제한 사항 일부 필드만 처리하는 생성자는 만들 수 없음 (항상 전체 필드를 대상으로 함)

 

예제: Compact Constructor로 유효성 검사하기

public record Employee(String name, int employeeNumber) {
    Employee {
        if (employeeNumber < 0) {
            throw new IllegalArgumentException("Employee number cannot be negative");
        }
    }
}

 

🔍 해석

  • 생성자 이름 Employee만 쓰고 (String name, int employeeNumber)는 생략했지만,
    → 컴파일러는 record 선언부에 있는 필드 정보를 기반으로 생성자를 자동 구성합니다.
  • this.name = name 등의 초기화 코드도 생략했지만,
    → 역시 컴파일러가 자동으로 필드에 값을 할당합니다.
  • 우리는 오직 추가적인 검증 로직만 작성하면 됩니다.

 

⚠️ 예외 발생 확인

Employee emp = new Employee("John", -5);

이렇게 실행하면, 음수 값이 들어가므로:

IllegalArgumentException: Employee number cannot be negative

예외가 발생합니다.
→ Compact Constructor를 통해 객체 생성 중 유효하지 않은 값에 대한 제어가 가능해진 것이죠.

 

✅ 즉, record도 생성자에서 검증을 할 수 있다!는 점에서 실무에 매우 유용합니다.


4. Records의 제약 사항

가능한 것

✅ static 필드 및 static 메서드 추가 가능
✅ 메서드 정의 가능
✅ implements를 사용해 인터페이스 구현 가능
✅ Compact Constructor 사용 가능

 

불가능한 것

🚫 extends 사용 불가능 (다른 클래스를 상속할 수 없음)
🚫 private 또는 non-static 필드 추가 불가능
🚫 mutable 필드 추가 불가능
🚫 abstract 메서드 선언 불가능

 

인터페이스 구현 예제

public record Employee(String name, int employeeNumber) implements Comparable<Employee> {
    @Override
    public int compareTo(Employee other) {
        return Integer.compare(this.employeeNumber, other.employeeNumber);
    }
}

Records는 언제 사용할까?

✅ 사용하기 좋은 경우

  • 데이터 전달 객체 (DTO)
  • 데이터 저장 클래스 (단순 값 저장)
  • 데이터베이스 엔티티
  • API 응답 모델

❌ 사용하지 않는 것이 좋은 경우

  • 도메인 객체 (비즈니스 로직 포함)
  • 필드를 변경해야 하는 경우 (Setter가 필요할 때)
  • 복잡한 로직이 포함된 클래스

결론

  • Java Records를 사용하면 데이터 저장용 클래스를 매우 간결하게 정의 가능.
  • 불변성을 유지하면서도 자동으로 필요한 메서드를 제공하여 코드 품질 향상.
  • 기능 확장이 필요하지 않은 단순한 데이터 저장 클래스에 적합.
  • Lombok을 사용하지 않아도 @Data와 같은 효과를 얻을 수 있음.

 

 


 

 

 

자주 묻는 질문 (Q&A)

Q1. 왜 레코드는 getter에 "get"이 붙지 않나요?

레코드는 기존 객체의 대체재가 아니라, 단순히 데이터를 전달하는 값 객체(Value Object) 개념에 초점이 맞춰져 있다.

✅ 일반적인 Java 클래스에서는 getName(), getEmployeeNumber()와 같은 게터 메서드를 사용하지만,
Records에서는 get이 생략되고 name() 또는 employeeNumber() 같은 방식으로 필드 값을 가져올 수 있다.

public record Employee(String name, int employeeNumber) {}

Employee emp = new Employee("John Doe", 12345);
System.out.println(emp.name());  // John Doe (getter 메서드처럼 작동)
System.out.println(emp.employeeNumber());  // 12345
 

이유
1️⃣ Records는 일반적인 객체 모델을 따르지 않으며, 단순한 데이터 컨테이너 역할을 수행한다.
2️⃣ Boilerplate 코드를 줄이기 위한 목적이므로, 불필요한 get 접두사를 생략하여 가독성을 높임.
3️⃣ Java 언어 자체의 설계 철학에 맞게, Records는 객체보다는 데이터 구조에 가깝게 설계됨.


Q2. Lombok 대신 레코드를 사용하면 되나요?

LombokRecords는 비슷한 기능을 제공하지만, 사용 목적과 장점이 다르다.
👉 둘 중 어떤 것을 선택할지 고민된다면 상황에 따라 적절한 선택이 필요하다.


비교 항목 Lombok Records
사용 목적 일반 객체의 Boilerplate 코드 감소 데이터 저장 및 전달용 객체 생성
주요 기능 @Data, @Builder, @Getter, @Setter 등 다양한 기능 지원 toString(), equals(), hashCode() 자동 생성
의존성 Lombok 라이브러리 설치 필요 JDK 16 이상에서 기본 제공
가독성 Lombok 어노테이션으로 코드가 줄어듦 가장 간결한 문법 (1줄로 클래스 생성 가능)
확장성 일반 클래스처럼 필드 추가, Setter 사용 가능 불변 객체이므로 Setter 없음
사용 가능 버전 Java 8 이상 Java 16 이상

 

📌 언제 Lombok을 사용할까?

✅ Setter가 필요할 때
✅ 객체가 비즈니스 로직을 포함하는 경우
✅ 추가적인 빌더 패턴(@Builder)이 필요할 때

 

📌 언제 Records를 사용할까?

✅ 단순한 데이터 저장과 전달이 목적일 때
✅ 코드 가독성을 높이고 유지보수를 편하게 하고 싶을 때
✅ 불변 객체(Immutable Object)가 필요할 때

 

 

🔹 예제: Lombok 사용

import lombok.Data;

@Data
public class Employee {
    private final String name;
    private final int employeeNumber;
}

 

🔹 예제: Records 사용

public record Employee(String name, int employeeNumber) {}

👉 같은 기능이지만, Records는 더 간결한 코드를 제공함.
👉 하지만, Setter가 필요하거나 객체의 상태를 변경해야 한다면 Lombok이 더 적합할 수 있음.


 

Q3. 레코드를 도메인 객체에 사용할 수 있을까요?

🚫 권장하지 않음!

도메인 객체(Domain Object)란, 비즈니스 로직을 포함하는 객체이다.
즉, 단순히 데이터를 저장하는 것뿐만 아니라, 특정 기능과 규칙을 수행해야 한다.

 

Records는 데이터 저장 및 전달을 위한 용도이므로, 비즈니스 로직을 포함하는 도메인 객체로 사용하기에는 적절하지 않다.
도메인 객체에는 일반 클래스 또는 Lombok을 사용하는 것이 더 적절하다.

 

 

📌 왜 Records를 도메인 객체로 사용하면 안 될까?

1️⃣ Setter가 없어 필드 값을 변경할 수 없음
→ 도메인 객체에서는 상태 변경이 필요한 경우가 많음 (예: 사용자의 점수 증가, 주문 상태 변경 등).
2️⃣ 비즈니스 로직을 추가하기 어려움
→ Records는 단순한 데이터 전달을 위해 설계되었으므로, 메서드를 추가하는 것이 비효율적임.
3️⃣ 도메인 객체는 상태를 바꿀 수 있어야 함
→ Records는 기본적으로 불변(Immutable)하므로, 상태 변경이 필요한 객체로 사용하기 어려움.

 

 

🔹 예제: 잘못된 사용 (Records를 도메인 객체로 사용)

public record Order(String orderId, String status) {
    public void changeStatus(String newStatus) { // 불가능한 설계
        this.status = newStatus; // ERROR ❌ (Setter 불가)
    }

대신 일반 클래스를 사용하는 것이 더 적절하다.

 

🔹 예제: 일반 클래스로 도메인 객체 구현

public class Order {
    private String orderId;
    private String status;

    public Order(String orderId, String status) {
        this.orderId = orderId;
        this.status = status;
    }

    public void changeStatus(String newStatus) {
        this.status = newStatus; // 상태 변경 가능 ✅
    }

    public String getStatus() {
        return status;
    }
}
 

 

📌 언제 Records를 사용하고, 언제 일반 클래스를 사용할까?

사용 사례 Records 사용 일반 클래스 사용
데이터 저장 ✅ DTO, API 응답 객체
데이터 변경 (Setter 필요) ✅ 가능
비즈니스 로직 포함 ✅ 가능
상태 유지 ❌ (Records는 Immutable) ✅ 가능
불변 객체가 필요할 때 ✅ (Setter 없음)

 

🔹 Records는 DTO(Data Transfer Object)에 적합하지만,
🔹 비즈니스 로직이 필요한 도메인 객체에는 적합하지 않음.


🔥 정리

질문 답변 요약
Q1. 왜 레코드는 getter에 "get"이 붙지 않나요? Records는 기존 객체 모델과 다르며, 데이터 저장용으로 설계되었기 때문에 불필요한 get을 생략하여 간결한 문법을 제공함.
Q2. Lombok 대신 레코드를 사용하면 되나요? 상황에 따라 다름. 간결한 코드가 필요하면 Records, 추가 기능이 필요하면 Lombok 선택.
Q3. 레코드를 도메인 객체에 사용할 수 있을까요? 권장하지 않음. 도메인 객체는 비즈니스 로직을 포함하므로 일반 클래스를 사용하는 것이 적절함.

 

Records는 단순한 데이터 저장 및 전달용으로 사용하면 매우 유용하지만, 일반 클래스의 완전한 대체는 아니다.
상황에 맞게 적절한 도구(Records, Lombok, 일반 클래스)를 선택하는 것이 중요하다!

 
 

 

'Web Programming Language > JAVA' 카테고리의 다른 글

BitSet 클래스  (0) 2025.02.17
DTO와 VO  (0) 2025.02.12
orElse vs orElseGet 차이점  (0) 2025.02.12
JAVA) print, printf, println 차이점  (0) 2021.03.11
JAVA) Java 프로그램 실행 구조  (0) 2020.11.18

+ Recent posts