반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 슬로우 쿼리
  • 싱글톤
  • 토비의 스프링
  • 팩터리 메서드 패턴
  • 참조 해제
  • 이펙티브자바
  • 느린 쿼리
  • item9
  • 프로그래머의 뇌
  • prerendering
  • 자바스크립트 면접
  • 객체복사
  • 자바
  • NEXT JS
  • 자바스크립트
  • pnpm
  • item7
  • 작업기억공간
  • item8
  • 이펙티브 자바
  • 외래키 제약조건
  • MSSQL
  • Java
  • 빈 검증
  • Pre-rendering
  • 디자인패턴
  • 메이븐 골
  • 추상 팩터리 패턴
  • 러스트
  • 이펙티브 자바 item9
  • next js app
  • 자바 검증
  • 스프링 검증
  • 메이븐 페이즈
  • Next.js
  • rust
  • serverless computing
  • 알고리즘
  • 도커공식문서
  • 싱글톤 패턴
  • 플라이웨이트패턴
  • 자바스크립트 인터뷰
  • try-with-resources
  • 자바 디자인패턴
  • Javadoc 자바독 자바주석 주석 Comment
  • 싱글턴
  • 자료구조
  • bean Validation
  • 서버리스 컴퓨팅
  • 메이븐 라이프사이클

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

빌더 패턴 (Builder Pattern) 이란?
Java/자바 디자인 패턴

빌더 패턴 (Builder Pattern) 이란?

2023. 1. 26. 00:29

빌더 패턴이란?

  • 객체 생성에 연관된 패턴이다.
  • 복잡한 객체들을 단계별로 생성할 수 있게 만들어 복잡도를 줄여준다.
  • 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법을 제공한다.

해결하려는 문제

  • 객체에 아주 많은 필드가 들어있어 복잡할 때 객체 생성이 어려운 문제가 있다.
    • 생성자에 필드를 몰아놓으면 처음 객체를 생성하기가 매우 어렵다.
    • Setter 를 이용하려고 하면, 필수값과 연계값 등 제약조건을 넣기 힘들다.

복잡한 객체를 생성할 때 빌더 패턴을 사용하면 조금 더 쉽게 생성할 수 있다.

빌더 패턴 다이어그램으로 살펴보기

  • 복잡한 Product 를 만드는 경우, Builder 에 추상 메서드를 추가하고, ConcreteBuilder 가 이를 구현하도록 위임하여 복잡한 객체를 만드는 프로세스를 독립적으로 분리시킬 수 있다.
  • Client 를 이용하지 않고, Director 를 이용하여 빌더를 사용하면, 빌더를 통해 Product 를 만드는 과정을 Client 와 더 멀어지도록 분리시킬 수 있다.

예제로 살펴보기

  • 월세를 구할 수 있는 부동산 앱을 만들려고 한다.
  • 집주인은 자신의 집을 올릴 수 있는데, 다양한 세부 정보를 설정할 수 있다.
  • 세부 정보는 선택사항이며, 전부 입력하지 않아도 된다.
    • 집 이름, 주소, 월세, 관리비만 필수이다.

House 객체 만들기

public class House {
    private String houseName;
    private String address;
    private float size;
    private int rooms;
    private int floor;
    private int topFloorOfBuilding;
    private int monthlyRent;
    private int maintenanceCost;
    private boolean hasParkingLot;
    private boolean hasAirConditioner;
    private boolean hasLaundryMachine;
}
  • 집 이름, 주소, 월세, 관리비는 필수이다.
  • 월세만 넣는 것은 불가능하고 항상 관리비를 같이 넣어야 한다.

HouseBuilder 인터페이스 만들기

public interface HouseBuilder {
    HouseBuilder name(String houseName);
    HouseBuilder address(String address);
    HouseBuilder totalCost(int monthlyRent, int maintenanceCost);
    HouseBuilder houseSize(float size, int rooms);
    HouseBuilder floor(int floor, int topFloorOfBuilding);
    HouseBuilder parkingLot(boolean hasParkingLot);
    HouseBuilder airConditioner(boolean hasAirConditioner);
    HouseBuilder laundryMachine(boolean hasLaundryMachine);
    House build();
}

DefaultHouseBuilder 구현체 만들기

public class DefaultHouseBuilder implements HouseBuilder {
    private String houseName;
    private String address;
    private float size;
    private int rooms;
    private int floor;
    private int topFloorOfBuilding;
    private int monthlyRent;
    private int maintenanceCost;
    private boolean hasParkingLot;
    private boolean hasAirConditioner;
    private boolean hasLaundryMachine;

    @Override
    public HouseBuilder name(String houseName) {
        this.houseName = houseName;
        return this;
    }

    @Override
    public HouseBuilder address(String address) {
        this.address = address;
        return this;
    }

    @Override
    public HouseBuilder totalCost(int monthlyRent, int maintenanceCost) {
        this.monthlyRent = monthlyRent;
        this.maintenanceCost = maintenanceCost;
        return this;
    }

    @Override
    public HouseBuilder houseSize(float size, int rooms) {
        this.size = size;
        this.rooms = rooms;
        return this;
    }

    @Override
    public HouseBuilder floor(int floor, int topFloorOfBuilding) {
        this.floor = floor;
        this.topFloorOfBuilding = topFloorOfBuilding;
        return this;
    }

    @Override
    public HouseBuilder parkingLot(boolean hasParkingLot) {
        this.hasParkingLot = hasParkingLot;
        return this;
    }

    @Override
    public HouseBuilder airConditioner(boolean hasAirConditioner) {
        this.hasAirConditioner = hasAirConditioner;
        return this;
    }

    @Override
    public HouseBuilder laundryMachine(boolean hasLaundryMachine) {
        this.hasLaundryMachine = hasLaundryMachine;
        return this;
    }

    @Override
    public House build() {
        return new House(
                houseName
                , address
                , size
                , rooms
                , floor
                , topFloorOfBuilding
                , monthlyRent
                , maintenanceCost
                , hasParkingLot
                , hasAirConditioner
                , hasLaundryMachine);
    }
}
  • HouseBuilder 의 메서드들을 구현해두었다.
  • 반환 타입이 HouseBuilder 라 메서드 체인 형태로 사용할 수 있다.

클라이언트에서 사용하기

public class App {
    public static void main(String[] args) {
        HouseBuilder houseBuilder = new DefaultHouseBuilder();

        House house = houseBuilder
                .address("북가좌동")
                .name("이랜드 해가든 아파트")
                .totalCost(2000000, 100000)
                .airConditioner(true)
                .laundryMachine(false)
                .parkingLot(true)
                .build();

        System.out.println(house);
    }
}
  • 선택적으로 넣을 값들을 메서드 체인으로 연결하여 객체를 생성했다.
  • 메서드 이름이 명확해 가독성도 좋은 편이다.

Director 객체 만들어보기

public class HouseDirector {
    private final HouseBuilder houseBuilder;

    public HouseDirector(HouseBuilder houseBuilder) {
        this.houseBuilder = houseBuilder;
    }

    public House gardenApartment() {
        return houseBuilder
                .address("북가좌동")
                .name("이랜드 해가든 아파트")
                .totalCost(2000000, 100000)
                .airConditioner(true)
                .laundryMachine(false)
                .parkingLot(true)
                .build();
    }
}
  • HouseBuilder 를 주입받아 이용하였다.

클라이언트 코드 변경하기

public class App {
    public static void main(String[] args) {
        HouseDirector houseDirector = new HouseDirector(new DefaultHouseBuilder());
        House house = houseDirector.gardenApartment();
        System.out.println(house);
    }
}
  • 클라이언트에는 더욱 구체적인 내용을 숨겼다.
  • 클라이언트는 houseDirector.gardenApartment() 메서드를 호출하면 해당 아파트 매물이 나온다는 것만 알면 된다.

빌더 패턴의 장점 알아보기

  • 이미 처음에 언급했듯, '만들기 복잡한 객체'를 순차적으로 만들 수 있는 방법을 제공해줄 수 있다.

인터페이스 반환 타입을 통해 순서 강제하기

public interface HouseBuilder {
    // 순서를 강제하는 예제
    HouseBuilderStep1 name(String houseName);
    HouseBuilderStep2 address(String address);
    HouseBuilderStep3 totalCost(int monthlyRent, int maintenanceCost);
    // 나머지 스텝들 ...
    House build();
}
  • 순서를 강제하면, 객체 설계자가 의도한대로 객체를 생성할 확률이 높아진다.

build() 단계에서 데이터 검증하기

@Override
public House build() {
    if (this.houseName == null) {
        throw new IllegalArgumentException("집 이름은 필수적으로 입력해야 하는 항목입니다.");
    }

    if (this.monthlyRent == 0 || this.maintenanceCost == 0) {
        throw new IllegalArgumentException("월세와 관리비는 필수적으로 입력해야 하는 항목입니다.");
    }

    // ...
}
  • 이 검증은 불완전한 객체를 생성하지 못하게 하는 안전장치가 될 수 있다.
  • build() 메서드에 검증을 추가하면 마지막 생성 시점에 올바르게 생성했는지 검증 가능하다.
  • 빈 객체를 만들고 Setter 를 이용하는 방법은 보통 값 하나씩만 검증이 가능한 반면, build() 메서드에서는 전체 값을 연계하여 값 검증이 가능하다.
    • ex) 주차장이 없는 경우, 월세와 관리비의 합이 5000000 을 넘으면 안된다고 가정했을 때, Setter 만을 이용해 검증한다면 setHasParkingLot(), setMonthlyRent(), setMaintenanceCost() 메서드 모두에 검증 장치를 심어두어야 한다.

Director 객체를 통해 구현 숨기기

  • Director 를 이용하여 복잡한 객체를 만드는 구체적인 과정을 숨길 수 있다.
    • 구체적 과정을 숨기지 않는다면, 클라이언트 코드의 전체적인 흐름보다 이쪽에 눈길이 가는 가독성이 떨어지는 코드가 탄생할 수 있다.

불변 객체를 편하게 만들 수 있다.

  • 불변 객체를 만들기 위해 final 필드를 많이 깔아두면, 생성자에서 final 필드를 모두 설정해주어야 하는 불편함이 있다.
    • 생성자는 오버로딩을 통해 여러개를 만드는 것이 가능하지만, 생성자 특징상 뒤에 넣어야 하는 인자가 너무 많아지며 가독성이 매우 떨어진다.
    • 빌더 패턴을 이용하면, build() 전, 모든 final 값이 들어왔는지 검증 후 객체를 만들면 간단하다.
public class House {
    private final String houseName;
    private final int monthlyRent;
    private final int maintenanceCost;
    // ...
}
public House gardenApartment() {
    return houseBuilder
            .address("북가좌동")
            .name("이랜드 해가든 아파트")
            .totalCost(2000000, 100000)
            .airConditioner(true)
            .laundryMachine(false)
            .parkingLot(true)
            .build();
}
  • final 로 선언된 필드가 있음에도 직접 생성자를 이용하지 않아도 된다.
  • 생성자는 메서드 이름에 의도를 나타낼 수도 없고 무조건 클래스 이름과 같아야 하기 때문에 파라미터가 길어질수록 가독성이 매우 떨어진다.

HouseBuilder 인터페이스를 통해 확장성을 꾀할 수 있다.

  • HouseBuilder 는 인터페이스기 때문에 어떤 클래스나 인터페이스에서도 상속이 가능하다.
  • 나중에 예제의 부동산 앱이 고도화된다면, 매물 별로 ApartmentBuilder, OfficetelBuilder, VillaBuilder 등을 생성하여 build() 시 필수 값들을 다르게 설정하여 확장할 수도 있다.

Lombok 을 사용하면 @Builder 애노테이션으로 쉽게 구현할 수 있다.

  • @Builder 애노테이션은 빌더 형태로 객체를 만드는 것을 도와준다.
@AllArgsConstructor
@Builder
public class House {
    @NonNull
    private final String houseName;
    private String address;
    private float size;
    private int rooms;
    private int floor;
    private int topFloorOfBuilding;
    private final int monthlyRent;
    private final int maintenanceCost;
    private boolean hasParkingLot;
    private boolean hasAirConditioner;
    private boolean hasLaundryMachine;
}
public class App {
    public static void main(String[] args) {
        House house = House.builder()
                .houseName("해가든 아파트")
                .monthlyRent(100)
                .maintenanceCost(10)
                .build();
        System.out.println(house);

    }
}
  • 객체에 .builder() 라는 메서드가 생기고 이를 통해 쉽게 빌드 패턴을 이용할 수 있게 된다.

빌더 패턴의 단점 알아보기

  • 객체를 생성하는 과정이 너무 장황해진다.
  • 구조가 복잡해진다
  • 부가적으로 생성해야 될 인터페이스와 클래스들이 생긴다.
    • Builder 인터페이스도 만들고 구현체도 만들고 Director 도 만들어야 할 수 있다.

자바 표준 라이브러리에서 사용하는 예제

자바 표준 라이브러리에서는 빌더 패턴을 어떻게 사용하고 있을까?

StringBuilder

  • StringBuilder 클래스는 toString() 을 호출하기 전까지 문자열에 대한 정보만 쌓아두다가 toString() 을 호출하면 마침내 String 타입의 객체를 생성한다.
    • 아래의 코드는 내부 코드 중 일부를 가져온 것이다.
public StringBuilder append(StringBuffer sb) {
        super.append(sb);
        return this;
}

public StringBuilder append(CharSequence s) {
    super.append(s);
    return this;
}

public StringBuilder append(CharSequence s, int start, int end) {
    super.append(s, start, end);
    return this;
}
  • 반환 타입이 전부 StringBuilder 이다.
  • 비슷한 예로 StringBuffer 가 있는데, StringBuilder 와는 synchronized 를 사용하냐 안하냐의 차이를 가지고 있다.
    • StringBuffer 는 synchronized 를 사용한다.
    • StringBuilder 는 synchronized 를 사용하지 않는다.
  • 또 비슷한 예로 StreamBuilder 도 있다.
Stream<String> names = Stream
        .<String>builder()
        .add("HELLO")
        .add("HELLO")
        .build();

메서드가 제네릭 타입을 반환한다면, 제네릭 타입에 넣어줄 인자 타입을 앞에 적어주어야 한다.

스프링에서 제공하는 UriComponentsBuilder

public class SpringExample {
    public static void main(String[] args) {
        UriComponents blogPath = UriComponentsBuilder.newInstance()
                .scheme("https")
                .host("jake-seo-dev.tistory.com")
                .path("/category/알고리즘 이론/배열과 문자열")
                .build().encode(); // https://jake-seo-dev.tistory.com/category/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%20%EC%9D%B4%EB%A1%A0/%EB%B0%B0%EC%97%B4%EA%B3%BC%20%EB%AC%B8%EC%9E%90%EC%97%B4
        System.out.println(blogPath);
    }
}
  • 스프링에서는 URI 컴포넌트를 쉽게 빌드할 수 있는 빌더를 제공한다.
  • 정적 메서드로 newInstance() 메서드 외에 fromPath(), fromUri() 등 많은 정적 팩토리 메서드를 제공한다.

레퍼런스

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard

반응형
저작자표시 비영리 (새창열림)

'Java > 자바 디자인 패턴' 카테고리의 다른 글

어댑터 패턴 (Adapter Pattern) 이란?  (1) 2023.01.29
프로토타입 패턴 (Prototype Pattern) 이란?  (2) 2023.01.28
추상 팩토리 패턴 (Abstract Factory Pattern) 이란?  (0) 2023.01.24
팩토리 메서드 패턴 (Factory Method Pattern) 이란?  (0) 2023.01.23
싱글톤 패턴 (Singleton Pattern) 이란?  (0) 2023.01.20
    'Java/자바 디자인 패턴' 카테고리의 다른 글
    • 어댑터 패턴 (Adapter Pattern) 이란?
    • 프로토타입 패턴 (Prototype Pattern) 이란?
    • 추상 팩토리 패턴 (Abstract Factory Pattern) 이란?
    • 팩토리 메서드 패턴 (Factory Method Pattern) 이란?
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바