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

orElse vs orElseGet

Java에서 Optional<T>을 사용할 때, orElse와 orElseGet은 Optional이 비어 있을 경우 기본값을 제공하는 메서드이다.
하지만 두 메서드가 작동하는 방식에는 중요한 차이점이 있다.

핵심 차이:

  • orElse(T other): 기본값을 항상 계산하고, Optional이 비어 있으면 반환.
  • orElseGet(Supplier<? extends T> supplier): Optional이 비어 있을 때만 기본값을 계산.

1. orElse 동작 방식

기본값을 "항상" 계산하는 메서드

  • Optional이 값이 있든 없든 항상 orElse에 전달된 값이 평가(계산)된다.
  • 즉, Optional이 값이 있을 때도 불필요한 연산이 실행될 수 있다.

orElse 예제

public class OrElseExample {
    public static String getDefaultValue() {
        System.out.println("getDefaultValue() 호출됨");
        return "기본값";
    }

    public static void main(String[] args) {
        Optional<String> optional = Optional.ofNullable("안녕하세요");

        String result = optional.orElse(getDefaultValue()); // getDefaultValue()가 실행될까?
        System.out.println("결과: " + result);
    }
}

실행 결과

getDefaultValue() 호출됨
결과: 안녕하세요

 

왜 이런 결과가 나왔을까?

  • Optional에는 "안녕하세요"라는 값이 이미 존재한다.
  • 그런데도 getDefaultValue()가 무조건 실행되었다.
  • 즉, 기본값이 필요하지 않더라도 연산이 발생하는 비효율적인 상황이 생길 수 있다.

2. orElseGet 동작 방식

기본값을 "필요할 때만" 계산하는 메서드

  • Optional이 비어 있을 때만 Supplier(람다 함수)를 실행한다.
  • 즉, 값이 존재하면 불필요한 연산을 하지 않는다.

orElseGet 예제

public class OrElseGetExample {
    public static String getDefaultValue() {
        System.out.println("getDefaultValue() 호출됨");
        return "기본값";
    }

    public static void main(String[] args) {
        Optional<String> optional = Optional.ofNullable("안녕하세요");

        String result = optional.orElseGet(() -> getDefaultValue()); // getDefaultValue()가 실행될까?
        System.out.println("결과: " + result);
    }
}

실행 결과

결과: 안녕하세요

 

이번에는 getDefaultValue()가 실행되지 않았다!

  • Optional에 "안녕하세요"라는 값이 있으므로 기본값을 만들 필요가 없기 때문이다.
  • 즉, orElseGet은 연산을 최소화할 수 있도록 도와준다.

 

3. orElse vs orElseGet 비교

비교 표

메서드 기본값 평가 시점 최적화 가능 여  언제 사용해야 할까?
orElse(T other) 항상 평가됨 ❌ 비효율적
(불필요한 연산 가능)
기본값이 가벼운 경우
orElseGet(Supplier<? extends T> supplier) 필요할 때만 평가됨 ✅ 최적화 가능 기본값을 생성하는 연산이 무거운 경우
(DB 조회, 파일 읽기 등)

orElse의 문제점

Optional<String> optional = Optional.ofNullable("데이터 있음");
String result = optional.orElse(getDataFromDatabase()); // DB 조회가 필요할까?
  • Optional에 값이 있음에도 DB 조회가 실행됩니다!
  • 연산 비용이 큰 작업이 불필요하게 실행될 수 있습니다.

해결 방법 → orElseGet 사용

Optional<String> optional = Optional.ofNullable("데이터 있음");
String result = optional.orElseGet(() -> getDataFromDatabase()); // 필요할 때만 실행됨!
  • Optional이 비어 있을 때만 DB 조회가 실행됩니다.
  • 연산 비용이 큰 작업일 경우, 반드시 orElseGet을 사용하세요.

4. 실전 예제: 성능 차이 확인

orElse vs orElseGet 비교 실험

public class OrElseVsOrElseGet {
    public static String expensiveOperation() {
        System.out.println("💰 비용이 큰 연산 실행 중...");
        return "연산 결과";
    }

    public static void main(String[] args) {
        System.out.println("📌 `orElse` 사용:");
        Optional<String> optional1 = Optional.of("값 있음");
        String result1 = optional1.orElse(expensiveOperation());

        System.out.println("\n📌 `orElseGet` 사용:");
        Optional<String> optional2 = Optional.of("값 있음");
        String result2 = optional2.orElseGet(() -> expensiveOperation());
    }
}

실행 결과

📌 `orElse` 사용:
💰 비용이 큰 연산 실행 중...

📌 `orElseGet` 사용:
  • orElse는 값이 있어도 expensiveOperation()이 실행됨. (비효율적❌)
  • orElseGet은 값이 있으면 실행되지 않음. (최적화 가능✅)

 

5. 언제 orElse를 쓰고 언제 orElseGet을 써야 할까?

기본값이 간단한 값이라면 → orElse 사용

String result = optional.orElse("기본값");
  • "기본값"처럼 가벼운 값이라면 orElse가 편리함.

기본값을 생성하는 연산이 무겁다면 → orElseGet 사용

String result = optional.orElseGet(() -> getDataFromDatabase());
  • getDataFromDatabase()처럼 실행 비용이 큰 경우, orElseGet을 사용하여 불필요한 연산을 방지.

 

결론

  • orElse는 기본값 연산이 항상 실행되므로, 연산 비용이 크다면 orElseGet을 사용하는 것이 좋다.
  • orElseGet은 Optional이 비어 있을 때만 연산하므로, 불필요한 연산을 방지할 수 있다.
  • 성능 최적화를 위해 기본값을 미리 계산할 필요가 없는 경우 orElseGet을 적극 활용하자!

 

참조
https://ysjune.github.io/posts/java/orelsenorelseget/

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

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

객체지향 프로그래밍(OOP) 이해하기

객체지향 프로그래밍(OOP)은 현실 세계를 객체로 추상화하여 소프트웨어를 설계하는 방법론이다. 코드의 재사용성을 높이고 유지보수를 쉽게 만들어 백엔드 개발에서 자주 사용된다.

 

객체지향 프로그래밍의 4대 원칙

1. 캡슐화 (Encapsulation):

  • 데이터를 외부에서 접근할 수 없도록 숨기고, 필요한 기능만 공개.
  • 데이터와 메서드를 객체 안에 묶어 관리.

예제: 

class Car {
  constructor(brand) {
    this.brand = brand; // private data
  }

  getBrand() {
    return this.brand; // public method
  }
}

const myCar = new Car("Tesla");
console.log(myCar.getBrand()); // "Tesla"

 

 

2. 추상화 (Abstraction):

  • 객체의 복잡성을 감추고, 필요한 핵심 기능만 노출.
  • 불필요한 내부 구현을 감추어 사용자에게 단순한 인터페이스 제공.

예제:

class Payment {
  processPayment(amount) {
    console.log(`Processing payment of $${amount}`);
  }
}

const payment = new Payment();
payment.processPayment(100); // 세부 구현은 감춰짐
 
3. 상속 (Inheritance):
  • 부모 클래스의 속성과 메서드를 자식 클래스가 물려받아 사용.
  • 코드 재사용성을 극대화.

예제:

class Animal {
  speak() {
    console.log("Animal speaks");
  }
}

class Dog extends Animal {
  speak() {
    console.log("Dog barks");
  }
}

const myDog = new Dog();
myDog.speak(); // "Dog barks"
 
 
4. 다형성 (Polymorphism):
  • 같은 이름의 메서드가 객체에 따라 다르게 동작.
  • 메서드 오버라이딩(Overriding)을 통해 구현.

예제:

class Shape {
  draw() {
    console.log("Drawing a shape");
  }
}

class Circle extends Shape {
  draw() {
    console.log("Drawing a circle");
  }
}

const shapes = [new Shape(), new Circle()];
shapes.forEach((shape) => shape.draw());
// Output:
// "Drawing a shape"
// "Drawing a circle"

 

캡슐화의 중요성

  • 데이터를 보호하여 외부 접근으로 인한 무결성 훼손 방지.
  • 인터페이스를 통해 객체의 사용 방법을 통제.

실생활 비유:

은행의 ATM은 내부 로직을 숨기고, 사용자에게 단순한 "돈 인출" 버튼만 제공

 

추상화의 활용

  • 복잡한 구현을 숨기고, 중요한 부분만 드러냄으로써 코드의 복잡성을 줄임.
  • 핵심 메서드만 사용자에게 노출.

실생활 비유:

자동차 운전자는 내부 엔진의 동작 방식을 몰라도 "운전대"와 "가속 페달"만으로 자동차를 조작할 수 있음.

 

상속과 코드 재사용

  • 상속을 통해 부모 클래스의 기능을 확장하고 새로운 기능을 추가.
  • 코드를 중복 작성하지 않고, 유지보수를 쉽게 만듦.

실생활 비유:

"동물"이라는 기본 클래스를 만들고, 이를 상속받아 "개", "고양이" 등의 세부 동물 클래스를 생성.

 

객체지향 프로그래밍의 장점

  1. 코드 재사용성 증가:
    중복 코드를 줄이고, 재사용 가능한 컴포넌트를 생성.
  2. 유지보수 용이:
    객체 단위로 코드를 관리하여 수정이 쉬움.
  3. 확장성:
    새로운 기능 추가 시 기존 코드를 최소한으로 수정.
  4. 현실 세계 모델링:
    현실 세계의 개념과 동작을 코드로 직관적으로 표현.

 

결론

객체지향 프로그래밍(OOP)은 코드의 재사용성, 유지보수성, 확장성을 높이는 데 매우 유용한 방법론이다.

캡슐화, 추상화, 상속, 다형성 등 핵심 원칙을 활용하여 효율적이고 직관적인 백엔드 시스템을 구축할 수 있다.

 

 

 

참조:
https://m.blog.naver.com/tkdldjs35/221299078193

https://www.youtube.com/watch?v=M3zlc6jiB5o

https://inpa.tistory.com/entry/OOP-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%B6%94%EC%83%81%ED%99%94-%EC%84%A4%EA%B3%84%EC%9D%98-%EC%9D%B4%ED%95%B4

+ Recent posts