본문 바로가기

멋쟁이사자처럼_부트캠프/Spring

[멋쟁이사자처럼 부트캠프 TIL 회고] 백엔드 부트캠프 13기: Java 52일차 Double(래퍼 클래스), 빌더 패턴(Builder Pattern)

기본형(primitive type)과 래퍼 클래스(wrapper class)의 차이

🔹 double (기본형, primitive type)

  • 8바이트(64비트) 크기의 부동소수점(floating-point) 타입.
  • 메모리 사용이 적고 연산 속도가 빠름.
  • null을 저장할 수 없음.
  • Java의 기본 데이터 타입이라서 객체가 아니라 값 자체를 저장함.

예제:

double pi = 3.14;
double result = pi * 2;
 

double은 산술 연산이 빠르지만, 객체처럼 사용하지 못함.

 

🔹 Double (래퍼 클래스, Wrapper Class)

  • double을 객체로 감싸는 래퍼 클래스.
  • Java의 객체 타입으로 null을 저장할 수 있음.
  • double과 달리 컬렉션(List, Map 등)에 저장 가능.
  • 메서드를 제공 (Double.valueOf(), Double.parseDouble() 등).

예제:

Double num = 3.14;  // 오토 박싱
Double nullValue = null; // 가능
double primitive = num;  // 오토 언박싱

 

🔹 주요 차이점 정리

구분 double (기본형) Double (래퍼 클래스)
메모리 사용 적음 많음 (객체 할당)
연산 속도 빠름 느림 (객체 관리)
null 허용 여부 불가능 가능
객체 컬렉션 저장 불가능 가능
메서드 지원 없음 parseDouble(), valueOf()
 

🔹 언제 double vs Double 사용해야 할까?

  • 연산이 많거나 성능이 중요한 경우 → double 사용
  • 객체가 필요하거나 null 값이 가능해야 하는 경우 → Double 사용 (예: 데이터베이스 값)

보통 기본적으로 double을 사용하고, null이 필요한 경우나 컬렉션에 저장해야 할 때만 Double을 사용하면 됨.

 

Double(래퍼 클래스)이 필요한 경우 예시

 

1. 데이터베이스 연동 (nullable 허용)

  • 데이터베이스에서 NULL 값을 허용해야 하는 경우, double(기본 타입)으로 선언하면 null을 저장할 수 없음.
  • Double(래퍼 클래스)을 사용하면 null 처리가 가능함.

예시: JPA 엔티티에서 Double 사용

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

    private String name;

    // 가격이 없는 경우 NULL을 저장할 수 있도록 Double 사용
    private Double price;

    // 기본 타입 사용 시 NULL을 허용하지 않음 (컴파일 오류 발생)
    // private double price;
}
  • 가격 정보가 없을 수도 있는 경우(null 허용) → Double 사용

 

2. JSON 직렬화/역직렬화 (null 허용)

  • Spring Boot에서 REST API를 만들 때, double은 기본값이 0.0으로 설정되지만,
    Double을 사용하면 null로 유지할 수 있음.

예시: JSON 요청 처리 (Double 사용)

@RestController
@RequestMapping("/products")
public class ProductController {
    @PostMapping
    public ResponseEntity<String> createProduct(@RequestBody ProductDTO productDTO) {
        return ResponseEntity.ok("Product created with price: " + productDTO.getPrice());
    }
}

@Data
public class ProductDTO {
    private String name;
    private Double price; // 클라이언트가 price를 생략할 경우 null이 될 수 있음
}
  • {"name": "Laptop"} → price가 생략된 경우 null 유지 가능

3. 컬렉션(List, Map)에서 값이 없을 경우 null 허용

  • double을 사용하면 기본적으로 0.0이 들어가기 때문에, 값이 없는 경우를 표현할 수 없음.
  • Double을 사용하면 null을 명확하게 표현 가능함.

예시: 평균 점수 계산 (null 허용)

public class Student {
    private String name;
    private Double averageScore; // null 허용

    public Student(String name, Double averageScore) {
        this.name = name;
        this.averageScore = averageScore;
    }

    public Double getAverageScore() {
        return averageScore;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Student> students = List.of(
            new Student("Alice", 85.5),
            new Student("Bob", null), // 시험 미응시
            new Student("Charlie", 92.0)
        );

        for (Student student : students) {
            System.out.println(student.getAverageScore());
        }
    }
}
  • Bob의 평균 점수가 null로 유지될 수 있음.

4. Optional과 함께 사용하여 값이 없는 경우 처리

  • Optional<Double>을 사용하면 null 체크를 좀 더 안전하게 할 수 있음.

예시: Optional로 평균 점수 처리

public Optional<Double> getStudentScore(Long studentId) {
    // 데이터베이스에서 조회했을 때 점수가 없을 수도 있음
    Double score = findScoreById(studentId);
    return Optional.ofNullable(score);
}

private Double findScoreById(Long studentId) {
    // DB 조회 로직 (점수가 없으면 null 반환)
    return null;
}
  • double을 사용하면 null을 표현할 수 없지만, Double을 사용하면 Optional.ofNullable()로 감싸서 처리 가능

정리

사용 케이스 double (기본 타입) Double (래퍼 클래스)
값이 null 있음 ❌ (기본값 0.0) ✅ (null 가능)
DB 연동 (@Entity) ❌ (null 허용 불가) ✅ (null 허용 가능)
JSON 직렬화 (@RequestBody) ❌ (0.0으로 처리됨) ✅ (null 유지 가능)
컬렉션(List, Map)에서 null 허용 ❌ (0.0 저장됨) ✅ (null 저장 가능)
Optional 사용 가능 여부 ✅ (Optional<Double> 사용 가능)
 

결론

  • Double(래퍼 클래스)은 null을 허용해야 하는 경우 (DB 연동, JSON 처리, Optional 사용 등)에서 사용.
  • double(기본 타입)은 절대 null이 될 수 없고, 성능상 원시 타입이 필요한 경우에 사용.

백엔드에서는 nullable한 데이터 처리 시에는 Double을 기본적으로 고려해야 함.

 

 


📌 빌더 패턴(Builder Pattern)이란?

빌더 패턴(Builder Pattern)객체의 생성 과정에서 생성자의 매개변수가 많거나, 일부 값만 선택적으로 설정해야 할 때 유용한 패턴 입니다.


1️⃣ 왜 빌더 패턴을 사용할까? (생성자 vs. Setter vs. 빌더 비교)

1) 생성자를 이용한 객체 생성 (문제점)

public class Pizza {
    private String size;
    private boolean cheese;
    private boolean pepperoni;
    private boolean mushrooms;

    public Pizza(String size, boolean cheese, boolean pepperoni, boolean mushrooms) {
        this.size = size;
        this.cheese = cheese;
        this.pepperoni = pepperoni;
        this.mushrooms = mushrooms;
    }
}

// 객체 생성
Pizza pizza = new Pizza("Large", true, false, true);

🔹 문제점: 매개변수 개수가 많아지면 가독성이 떨어지고, 순서를 헷갈릴 위험이 있음.


2) Setter를 이용한 객체 생성 (문제점)

public class Pizza {
    private String size;
    private boolean cheese;
    private boolean pepperoni;
    private boolean mushrooms;

    public void setSize(String size) { this.size = size; }
    public void setCheese(boolean cheese) { this.cheese = cheese; }
    public void setPepperoni(boolean pepperoni) { this.pepperoni = pepperoni; }
    public void setMushrooms(boolean mushrooms) { this.mushrooms = mushrooms; }
}

// 객체 생성
Pizza pizza = new Pizza();
pizza.setSize("Large");
pizza.setCheese(true);
pizza.setPepperoni(false);
pizza.setMushrooms(true);

🔹 문제점: 객체가 완전히 생성되기 전에 불완전한 상태로 남을 수 있음 (Immutable X)
🔹 문제점: 객체의 일관성이 깨질 위험 이 있음.


3) 빌더 패턴을 이용한 객체 생성 (해결책)

public class Pizza {
    private String size;
    private boolean cheese;
    private boolean pepperoni;
    private boolean mushrooms;

    // Private 생성자 (외부에서 직접 호출 X)
    private Pizza(Builder builder) {
        this.size = builder.size;
        this.cheese = builder.cheese;
        this.pepperoni = builder.pepperoni;
        this.mushrooms = builder.mushrooms;
    }

    // Builder 클래스 (정적 내부 클래스)
    public static class Builder {
        private String size;
        private boolean cheese = true; // 기본값
        private boolean pepperoni = true;
        private boolean mushrooms = true;

        public Builder size(String size) {
            this.size = size;
            return this;
        }

        public Builder cheese(boolean cheese) {
            this.cheese = cheese;
            return this;
        }

        public Builder pepperoni(boolean pepperoni) {
            this.pepperoni = pepperoni;
            return this;
        }

        public Builder mushrooms(boolean mushrooms) {
            this.mushrooms = mushrooms;
            return this;
        }

        public Pizza build() {
            return new Pizza(this);
        }
    }

    public static Builder builder() {
        return new Builder();
    }

    @Override
    public String toString() {
        return "Pizza{" +
                "size='" + size + '\'' +
                ", cheese=" + cheese +
                ", pepperoni=" + pepperoni +
                ", mushrooms=" + mushrooms +
                '}';
    }
}

// 객체 생성
Pizza pizza = Pizza.builder()
        .size("Large")
        .cheese(true)
        .pepperoni(false)
        .mushrooms(true)
        .build();

System.out.println(pizza);

장점:
가독성이 좋다 (어떤 값을 설정하는지 명확함)
불변성(Immutable) 보장 (객체 생성 후 값 변경 불가능)
필수값과 선택값을 분리 가능 (필수값만 설정하고, 나머지는 기본값 사용 가능)


2️⃣ 빌더 패턴이 유용한 경우

🔹 매개변수가 많을 때 → 생성자의 매개변수가 많으면 가독성이 떨어지므로 빌더 패턴이 적합함.
🔹 객체의 불변성을 유지해야 할 때 → 객체 생성 이후 값을 변경하지 않도록 할 때 유용함.
🔹 선택적 매개변수를 제공할 때 → 어떤 값만 선택적으로 설정하고 싶을 때 유용함.


3️⃣ Lombok의 @Builder 사용 (더 간단하게!)

Lombok을 사용하면 자동으로 빌더 패턴을 적용 할 수 있음.

import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class Pizza {
    private String size;
    @Builder.Default
    private boolean cheese = true;
    @Builder.Default
    private boolean pepperoni = true;
    @Builder.Default
    private boolean mushrooms = true;
}

// 객체 생성
Pizza pizza = Pizza.builder()
        .size("Large")
        .cheese(true)
        .pepperoni(false)
        .mushrooms(true)
        .build();

System.out.println(pizza);

장점:
✅ 코드가 훨씬 짧아짐
빌더 자동 생성 → @Builder만 붙이면 됨


4️⃣ 정리 (빌더 패턴을 써야 하는 이유)

방식 장점 단점
생성자 한 번에 객체 생성 가능 매개변수가 많아지면 가독성↓, 실수 위험
Setter 개별적으로 값 설정 가능 불변성 X, 일관성 깨질 위험
Builder 가독성↑, 불변성 유지, 선택적 매개변수 설정 가능 코드가 조금 길어질 수 있음

결론: 매개변수가 많거나, 객체의 불변성을 유지하고 싶다면 빌더 패턴이 가장 좋은 선택임.