티스토리 뷰

 

최근 자바 생태계는 '장황한 언어'라는 오명을 벗기 위해 대대적인 변화를 거듭하고 있습니다. 특히 데이터 중심의 프로그래밍이 강조되면서, 불필요한 보일러플레이트(반복적인 코드)를 줄이고 데이터의 구조를 명확히 정의하려는 노력이 계속되고 있죠. 그 중심에 서 있는 것이 바로 RecordsSealed Classes입니다.

이 두 기능은 단순히 타이핑을 줄여주는 도구가 아닙니다. 코드의 의도를 명확히 하고, 컴파일러가 우리의 로직을 검증할 수 있게 돕는 강력한 무기입니다.


1. Deep Dive: 왜 Records와 Sealed Classes인가?

과거의 자바에서 데이터 운반 객체(DTO) 하나를 만들려면 Getter, Setter, equals, hashCode, toString까지 수십 줄의 코드가 필요했습니다. 이는 가독성을 떨어뜨리고 버그가 숨어들 틈을 만들었죠.

Records: 불변 데이터의 표준화

Records는 "데이터를 보관하기 위한 클래스"라는 본질에 집중합니다. 클래스 선언 시 필드만 정의하면 컴파일러가 모든 관례적인 메서드를 자동으로 생성해 줍니다. 핵심은 **불변성(Immutability)**입니다. 한 번 생성된 데이터는 변하지 않는다는 신뢰를 코드 전체에 부여합니다.

Sealed Classes: 도메인의 경계를 긋다

Sealed Classes는 상속 계층을 엄격하게 제한합니다. "이 클래스는 A, B, C 외에는 상속받을 수 없다"라고 못 박는 것이죠. 이는 현실 세계의 비즈니스 규칙을 코드에 그대로 이식할 수 있게 해줍니다.

비유로 이해하기

  • Records는 '밀봉된 영수증'과 같습니다. 품목과 가격이 적혀 있고, 한 번 발행되면 내용을 수정할 수 없으며 그 자체로 완전한 정보를 담고 있죠.
  • Sealed Classes는 '결제 수단'이라는 상자입니다. 이 상자 안에는 오직 '카드', '현금', '포인트'라는 세 가지 카드만 들어올 수 있도록 입구를 제한해둔 것과 같습니다. 다른 엉뚱한 것이 들어올 여지를 아예 차단하는 것이죠.

2. Hands-on: 실전 이커머스 주문 시스템 구현

실제 이커머스 환경에서 주문의 상태와 결제 수단을 모델링하는 예제를 통해 이 기술들을 어떻게 활용하는지 살펴보겠습니다.

데이터 구조 설계

Java
 
// 결제 수단을 Sealed Interface로 정의하여 종류를 제한
public sealed interface PaymentMethod permits Card, Cash, Point {}

// 각 결제 수단은 Record로 구현하여 불변 데이터로 관리
public record Card(String cardNumber, String bankName) implements PaymentMethod {}
public record Cash(long amount) implements PaymentMethod {}
public record Point(String userId, long pointBalance) implements PaymentMethod {}

// 주문 정보를 담는 Record
public record Order(
    String orderId,
    long totalPrice,
    PaymentMethod paymentMethod // Sealed Type 사용으로 안전성 확보
) {}

비즈니스 로직 적용 (Pattern Matching)

Java
 
public class OrderProcessor {
    public void processOrder(Order order) {
        // Sealed Class와 Pattern Matching을 결합하면 switch 문에서 모든 케이스를 강제할 수 있음
        String result = switch (order.paymentMethod()) {
            case Card c -> String.format("카드 결제 진행: %s (%s)", c.bankName(), c.cardNumber());
            case Cash m -> "현금 결제 접수: " + m.amount() + "원";
            case Point p -> "포인트 차감 결제: 사용자ID " + p.userId();
            // default 문이 필요 없음! 모든 PaymentMethod 구현체를 다뤘기 때문
        };
        
        System.out.println("주문 처리 상태: " + result);
    }
}

💡 트러블슈팅 팁

  • 컴파일 에러 발생 시: 만약 PaymentMethod에 Coupon이라는 구현체를 새로 추가했는데 switch 문에서 처리하지 않았다면, 자바 컴파일러는 즉시 에러를 발생시킵니다. 이는 런타임에 발생할 수 있는 '처리되지 않은 케이스' 오류를 사전에 방지해 주는 강력한 안전장치입니다.
  • Record의 확장성: Record는 상태를 상속받을 수 없습니다. 만약 공통 필드가 너무 많아 상속 구조가 반드시 필요하다면 일반 클래스를 고려해야 하지만, 대부분의 데이터 전달 객체는 Record만으로도 충분합니다.

3. Trade-offs: 고려해야 할 사항

모든 기술에는 비용이 따릅니다. 무조건적인 도입보다는 상황에 맞는 판단이 필요합니다.

  • Records의 한계: 모든 필드가 final입니다. 즉, 객체 생성 후 상태를 변경해야 하는 JPA 엔티티(Entity) 등에는 적합하지 않을 수 있습니다. 주로 DTO, VO, 메시지 객체에 최적화되어 있습니다.
  • Sealed Classes의 폐쇄성: 라이브러리 제작자라면 사용자가 자유롭게 확장해야 하는 인터페이스에는 Sealed를 사용해서는 안 됩니다. 하지만 도메인 로직처럼 비즈니스 규칙이 명확한 경우에는 최고의 선택입니다.

4. 요약 및 제언

Records와 Sealed Classes는 자바를 더욱 안전하고 간결한 언어로 만들어 주었습니다.

  1. Records를 통해 데이터의 투명성과 불변성을 확보하세요.
  2. Sealed Classes를 통해 비즈니스 도메인의 유한한 상태를 엄격하게 관리하세요.
  3. 이 둘을 결합하면 **대수적 데이터 타입(Algebraic Data Types)**의 이점을 자바에서도 누릴 수 있습니다.

여러분의 프로젝트에서 가장 먼저 Record로 전환할 수 있는 데이터 객체는 무엇인가요? 반복적인 코드를 제거하는 것만으로도 비즈니스 로직의 핵심이 훨씬 선명하게 보이기 시작할 것입니다.

현대적 자바의 문법을 활용해 더 견고한 아키텍처를 설계해 보시길 바랍니다.

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
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
글 보관함