Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
Tags
- 가용영역
- React
- HashMap
- axios
- 코엑스그랜드볼룸
- 포트앤어댑터 아키텍처
- 글쓰기세미나
- static-factory-method
- 코딩테스트
- 글또
- 클라우드아키텍처
- QueryDSL
- ReverseNested
- object-creation
- DevOps
- 3계층 아키텍처
- 클린 아키텍처
- 프로그래머스
- builder-pattern
- SpringBoot
- 헥사고날 아키텍처
- design-pattern
- UserLand
- OpenSearch
- Level2
- 레벨1
- 글또10기
- 회고
- 다짐글
- constructor
Archives
- Today
- Total
oguri's garage
(Java) Record - Lombok Builder vs Record 비교 본문
1. Record란 무엇인가?
1.1 핵심 개념
Java Record는 "불변 데이터 상자"와 같다.
Record 클래스는 일반 클래스보다 적은 의식(ceremony)으로 단순한 데이터 집합을 모델링하는 데 도움이 되는 특별한 종류의 클래스입니다.
Record의 본질:
- 데이터 캐리어: 단순히 데이터를 운반하는 목적
- 불변성: 한 번 생성되면 값 변경 불가
- 자동 생성: getter, equals, hashCode, toString 자동 제공
1.2 공식 정의
Record 클래스는 고정된 값 집합(record components라고 함)에 대한 얕은 불변의 투명한 캐리어입니다.
2. 코드 비교: Lombok Builder vs Record
2.1 기존 Lombok Builder 방식
@Getter
@Builder
public class PdfDownloadData {
private final ByteArrayResource resource;
private final String originalFilename;
private final String displayFilename;
private final long contentLength;
}
// 사용법
PdfDownloadData data = PdfDownloadData.builder()
.resource(resource)
.originalFilename("test.pdf")
.displayFilename("report.pdf")
.contentLength(1024)
.build();
2.2 Record 방식
public record PdfDownloadData(
Resource resource,
String originalFilename,
String displayFilename,
long contentLength
) {
// 검증 로직을 포함한 compact constructor
public PdfDownloadData {
Objects.requireNonNull(resource, "resource must not be null");
Objects.requireNonNull(originalFilename, "originalFilename must not be null");
Objects.requireNonNull(displayFilename, "displayFilename must not be null");
if (contentLength < 0) {
throw new CyturException("TI40001");
}
}
}
// 사용법 - 더 간단하고 직관적
PdfDownloadData data = new PdfDownloadData(resource, "test.pdf", "report.pdf", 1024);
3. Record의 자동 생성 메서드
3.1 자동으로 생성되는 것들
Record 클래스는 헤더에서 설명을 지정하면, 적절한 접근자, 생성자, equals, hashCode, toString 메서드가 자동으로 생성됩니다.
// 이 간단한 record 선언이...
public record Point(int x, int y) {}
// 다음과 동일한 클래스를 자동 생성합니다:
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; } // getter가 아닌 accessor
public int y() { return y; }
@Override
public boolean equals(Object obj) { /* 자동 구현 */ }
@Override
public int hashCode() { /* 자동 구현 */ }
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
4. Compact Constructor (압축 생성자)
4.1 기본 개념
Compact Constructor는 "입구에서 검문하는 경비원"과 같습니다. 데이터가 들어올 때 유효성을 검증하고, 필요한 전처리를 수행합니다.
4.2 일반 생성자 vs Compact Constructor
// ❌ 일반 생성자 - 중복이 많음
public record Rectangle(double length, double width) {
public Rectangle(double length, double width) {
if (length <= 0 || width <= 0) {
throw new IllegalArgumentException("Invalid dimensions");
}
this.length = length; // 중복된 할당
this.width = width; // 중복된 할당
}
}
// ✅ Compact Constructor - 간결하고 명확
public record Rectangle(double length, double width) {
public Rectangle { // 매개변수 생략
if (length <= 0 || width <= 0) {
throw new IllegalArgumentException(
String.format("Invalid dimensions: %f, %f", length, width)
);
}
// this.length = length; 자동으로 처리됨
// this.width = width; 자동으로 처리됨
}
}
4.3 실제 프로젝트 적용 예시
public record PdfDownloadData(
Resource resource,
String originalFilename,
String displayFilename,
long contentLength
) {
public PdfDownloadData {
// null 검증
Objects.requireNonNull(resource, "resource must not be null");
Objects.requireNonNull(originalFilename, "originalFilename must not be null");
Objects.requireNonNull(displayFilename, "displayFilename must not be null");
// 비즈니스 규칙 검증
if (contentLength < 0) {
throw new CyturException("TI40001");
}
// 파일명 정규화 (필요시)
originalFilename = originalFilename.trim();
displayFilename = displayFilename.trim();
}
}
5. Record의 고급 활용
5.1 메서드 추가
public record PdfDownloadData(
Resource resource,
String originalFilename,
String displayFilename,
long contentLength
) {
// 검증 로직
public PdfDownloadData {
Objects.requireNonNull(resource, "resource must not be null");
if (contentLength < 0) {
throw new CyturException("TI40001");
}
}
// 커스텀 메서드 추가
public boolean isLargeFile() {
return contentLength > 10 * 1024 * 1024; // 10MB 이상
}
public String getFormattedSize() {
return String.format("%.2f MB", contentLength / (1024.0 * 1024.0));
}
// Static 팩토리 메서드
public static PdfDownloadData create(Resource resource, String filename, long size) {
return new PdfDownloadData(resource, filename, filename, size);
}
}
5.2 인터페이스 구현
public record PdfDownloadData(
Resource resource,
String originalFilename,
String displayFilename,
long contentLength
) implements Downloadable, Serializable {
@Override
public void download(HttpServletResponse response) {
response.setHeader("Content-Disposition",
"attachment; filename=\"" + displayFilename + "\"");
// 다운로드 로직
}
}
5.3 제네릭 Record
public record ApiResponse<T>(
T data,
String message,
int statusCode
) {
public ApiResponse {
Objects.requireNonNull(data, "data must not be null");
if (statusCode < 100 || statusCode >= 600) {
throw new IllegalArgumentException("Invalid HTTP status code");
}
}
public boolean isSuccess() {
return statusCode >= 200 && statusCode < 300;
}
}
// 사용 예시
ApiResponse<PdfDownloadData> response = new ApiResponse<>(
pdfData, "Success", 200
);
6. Record vs Lombok Builder 비교
| 측면 | Record | Lombok Builder |
|---|---|---|
| 코드 길이 | 매우 짧음 | 중간 길이 |
| 컴파일 타임 | 빠름 (언어 차원 지원) | 느림 (애노테이션 프로세싱) |
| IDE 지원 | 완벽 | 플러그인 필요 |
| 불변성 | 강제됨 | 선택적 |
| 생성자 검증 | Compact Constructor | @Builder.Default, 커스텀 빌더 |
| 가독성 | 매우 높음 | 높음 |
| 빌더 패턴 | 미지원 | 완벽 지원 |
| null 안전성 | 수동 검증 필요 | @NonNull 지원 |
7. 언제 Record를 사용할까?
7.1 Record 사용 권장 상황
// 1. 단순 데이터 전송 객체 (DTO)
public record UserDto(Long id, String name, String email) {}
// 2. API 응답 객체
public record ApiError(String code, String message, LocalDateTime timestamp) {}
// 3. 설정 데이터
public record DatabaseConfig(String url, String username, int maxConnections) {}
// 4. 이벤트 데이터
public record FileDownloadEvent(String filename, String userId, LocalDateTime timestamp) {}
7.2 Record 사용 지양 상황
// 1. 가변 데이터가 필요한 경우
public class UserSession {
private String sessionId;
private LocalDateTime lastAccess; // 계속 업데이트됨
// Record로는 불가능
}
// 2. 복잡한 빌더 패턴이 필요한 경우
QueryBuilder.select("name", "email")
.from("users")
.where("age > 18")
.orderBy("name")
.build(); // Record로는 복잡함
// 3. 상속이 필요한 경우
public class Employee extends Person { } // Record는 final 클래스
8. 실무 활용 패턴
8.1 검증과 함께 사용
public record CreateUserRequest(
@NotBlank String username,
@Email String email,
@Size(min = 8) String password
) {
public CreateUserRequest {
// 추가 비즈니스 검증
if (username.toLowerCase().contains("admin")) {
throw new IllegalArgumentException("Username cannot contain 'admin'");
}
}
}
8.2 팩토리 메서드 패턴
public record PdfDownloadData(
Resource resource,
String originalFilename,
String displayFilename,
long contentLength
) {
// 다양한 생성 방법 제공
public static PdfDownloadData fromFile(File file) throws IOException {
byte[] content = Files.readAllBytes(file.toPath());
ByteArrayResource resource = new ByteArrayResource(content);
return new PdfDownloadData(
resource,
file.getName(),
file.getName(),
content.length
);
}
public static PdfDownloadData fromS3(S3Object s3Object, String displayName) throws IOException {
byte[] content = s3Object.getObjectContent().readAllBytes();
ByteArrayResource resource = new ByteArrayResource(content);
return new PdfDownloadData(
resource,
s3Object.getKey(),
displayName,
content.length
);
}
}
8.3 중첩 Record 활용
public record ApiResponse<T>(
T data,
Meta meta,
Error error
) {
public record Meta(
String requestId,
LocalDateTime timestamp,
String version
) {}
public record Error(
String code,
String message,
String detail
) {}
// 성공 응답 팩토리
public static <T> ApiResponse<T> success(T data, String requestId) {
return new ApiResponse<>(
data,
new Meta(requestId, LocalDateTime.now(), "1.0"),
null
);
}
// 실패 응답 팩토리
public static <T> ApiResponse<T> error(String code, String message) {
return new ApiResponse<>(
null,
new Meta(UUID.randomUUID().toString(), LocalDateTime.now(), "1.0"),
new Error(code, message, null)
);
}
}
9. 마이그레이션 가이드: Lombok → Record
9.1 단계별 변환 과정
Step 1: 기본 변환
// Before - Lombok
@Getter
@Builder
public class PdfDownloadData {
private final ByteArrayResource resource;
private final String originalFilename;
private final String displayFilename;
private final long contentLength;
}
// After - Record (기본)
public record PdfDownloadData(
ByteArrayResource resource,
String originalFilename,
String displayFilename,
long contentLength
) {}
Step 2: 검증 로직 추가
// Before - Lombok with validation
@Getter
@Builder
public class PdfDownloadData {
@NonNull private final ByteArrayResource resource;
@NonNull private final String originalFilename;
@NonNull private final String displayFilename;
private final long contentLength;
// 별도 검증 메서드 필요
public void validate() {
if (contentLength < 0) {
throw new CyturException("TI40001");
}
}
}
// After - Record with compact constructor
public record PdfDownloadData(
Resource resource,
String originalFilename,
String displayFilename,
long contentLength
) {
public PdfDownloadData {
Objects.requireNonNull(resource, "resource must not be null");
Objects.requireNonNull(originalFilename, "originalFilename must not be null");
Objects.requireNonNull(displayFilename, "displayFilename must not be null");
if (contentLength < 0) {
throw new CyturException("TI40001");
}
}
}
Step 3: 메서드 추가 (필요시)
public record PdfDownloadData(
Resource resource,
String originalFilename,
String displayFilename,
long contentLength
) {
// 검증 로직
public PdfDownloadData {
Objects.requireNonNull(resource, "resource must not be null");
Objects.requireNonNull(originalFilename, "originalFilename must not be null");
Objects.requireNonNull(displayFilename, "displayFilename must not be null");
if (contentLength < 0) {
throw new CyturException("TI40001");
}
}
// 빌더 패턴이 필요한 경우 팩토리 메서드로 대체
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Resource resource;
private String originalFilename;
private String displayFilename;
private long contentLength;
public Builder resource(Resource resource) {
this.resource = resource;
return this;
}
public Builder originalFilename(String originalFilename) {
this.originalFilename = originalFilename;
return this;
}
public Builder displayFilename(String displayFilename) {
this.displayFilename = displayFilename;
return this;
}
public Builder contentLength(long contentLength) {
this.contentLength = contentLength;
return this;
}
public PdfDownloadData build() {
return new PdfDownloadData(resource, originalFilename, displayFilename, contentLength);
}
}
}
10. 고급 활용 패턴
10.1 조건부 검증
public record UserRegistrationData(
String username,
String email,
String password,
boolean isAdmin
) {
public UserRegistrationData {
Objects.requireNonNull(username, "Username is required");
Objects.requireNonNull(email, "Email is required");
// 조건부 검증
if (isAdmin && !email.endsWith("@company.com")) {
throw new IllegalArgumentException("Admin users must use company email");
}
// 데이터 정규화
username = username.toLowerCase().trim();
email = email.toLowerCase().trim();
}
}
10.2 계산된 필드
public record OrderItem(
String productName,
int quantity,
double unitPrice
) {
// 계산된 값을 메서드로 제공
public double totalPrice() {
return quantity * unitPrice;
}
public double totalPriceWithTax(double taxRate) {
return totalPrice() * (1 + taxRate);
}
}
10.3 패턴 매칭과 함께 사용 (Java 17+)
public sealed interface Result<T> permits Success, Error {
record Success<T>(T data) implements Result<T> {}
record Error<T>(String message, String code) implements Result<T> {}
}
// 패턴 매칭 사용
public String handleResult(Result<String> result) {
return switch (result) {
case Result.Success<String>(var data) -> "Success: " + data;
case Result.Error<String>(var message, var code) -> "Error " + code + ": " + message;
};
}
11. 학습 포인트
11.1 Record 사용시 주의사항
- 불변성: 한 번 생성되면 값 변경 불가
- final 클래스: 상속 불가능
- 인스턴스 필드: 추가 인스턴스 변수 선언 불가
- accessor 네이밍:
getX()대신x()사용
11.2 성능 고려사항
// 메모리 효율적인 Record 사용
public record LightweightData(int id, String name) {} // 작고 효율적
// 큰 객체는 주의
public record HeavyData(
byte[] largeArray, // 주의: 큰 배열
List<String> items // 주의: 큰 컬렉션
) {
public HeavyData {
// 방어적 복사가 필요할 수 있음
largeArray = largeArray.clone();
items = List.copyOf(items);
}
}'개발하다 > Java' 카테고리의 다른 글
| Java 문자열 조합, String.format()이 가독성에 좋은 이유 (0) | 2025.10.19 |
|---|---|
| (Java) Stream API에서 Checked Exception 처리하기 (0) | 2025.10.07 |
| `null` 대신 `Optional.empty`를 return 해야 하는 이유 (0) | 2025.09.30 |
| Java 객체 생성 패턴 비교 및 선택 가이드 (0) | 2025.09.28 |
| HashMap 사용 방법 (5) | 2024.05.28 |