• [디자인패턴]빌더 패턴(Builder Pattern)

    2021. 3. 21.

    by. 웰시코더

    반응형

    -객체를 생성하는 패턴 중 하나인 빌더 패턴(Builder Pattern)을 알아본다.

    -빌더 패턴 이전에 객체를 생성하는 가장 일반적인 패턴인 점층적 생성자 패턴, 자바빈 패턴을 먼저 알아본다.

    -이번 포스팅은 기계인간 John Grib님의 블로그를 많이 참고하여 학습하였다.(출처는 아래에 표시)


    빌더 패턴이란?

     객체를 생성하는 방법은 여러가지가 있는데, 일반적으로 생성자에 인자를 넣어 인스턴스를 생성하는 방법이나 setter를 이용하는 자바빈 패턴을 많이 사용한다. 빌더패턴은 이런 패턴들의 단점을 좀 더 개선한 방법이다. 우선 빌더 패턴을 소개하기 전에 점층적 생성자 패턴, 자바빈 패턴을 소개한 후, 이들의 단점을 개선한 빌더 패턴을 마지막으로 소개한다. 

    점층적 생성자 패턴

     생성자를 통한 인스턴스 생성 방식이다. 인자의 개수에 따라 경우의 수만큼 생성자를 만드는 방식이다. 예를 들어 인자가 a,b,c,d가 있다고 가정하면 new Test(a), new Test(a,b), new Test(a,b,c), new Test(a,b,c,d) 등으로 인자가 들어갈 수 있는 모든 조합으로 생성자를 만들어 필요에 따라 생성자를 호출한다. 예시는 Coffee 클래스의 name, price, taste,milkYn(우유포함여부)를 셋팅하는 예시로 학습한다.

     

     

    Coffee.java

    public class Coffee {
    	private final String name;
    	private final int price;
    	private final String taste;
    	private final String milkYn;
    	
    	public Coffee(String name, int price) {
    		this.name = name;
    		this.price = price;
    	}
    	
    	public Coffee(String name, int price, String taste, String milkYn) {
    		this.name = name;
    		this.price = price;
    		this.taste = taste;
    		this.milkYn = milkYn;
    	}
        
        @Override
    	public String toString() {
    		return "name:" + name + " " + "price:" + price + " " + "taste:" + taste + " " + "milkYn:" + milkYn;
    	}
    }

     커피의 필수적인 인자인 name, price만 인자로 받는 Coffee 생성자, 그리고 모든 인자를 다 받는 Coffee 생성자 2개를 선언하였다. 실제로는 경우의 수에 따라 name,price,tatste를 받는 생성자도 만들어야 하는데 여기서는 간단하게 필수인자(name,price)와 모든인자(name,price,taste,milkYn)으로 나누었다. 또한 생성자 방식이기 때문에 한 번 생성한 후 값 변경이 없기 때문에 final로 프로퍼티 선언한다.

     

    이 패턴의 장점은 다음과 같다.

    • new Coffee("아메리카노", 5000, "미지정", "미지정")과 같은 방식의 호출이 많은 경우, new Coffee("아메리카노, 5000)의 생성자로 객체 생성이 가능

    이 패턴의 단점은 다음과 같다.

    • 객체의 프로퍼티가 많으면 생성자를 그만큼 많이 만들어야 함
    • 프로퍼티가 추가되면 그만큼 경우의 수에 맞게 생성자를 만들어야 함
    • 인자에 대한 설명이 없기 때문에 인자가 많은 경우 몇번째 인자가 어떤 의미인지 헷깔림

    자바빈 패턴

     점층적 생성자 패턴의 단점을 보완하기 위한 패턴이다. setter, getter가 있으며 웹어플리케이션 개발 때 자주 쓰는 dto를 이런 자바빈 패턴을 이용하여 많이 만든다.

     

    CoffeeJavaBean.java

    package Build;
    
    public class CoffeeJavaBean {
    	private String name;
    	private int price;
    	private String taste;
    	private String milkYn;
    	
    	public CoffeeJavaBean() {
    		// TODO Auto-generated constructor stub
    	}
    	
    	public String getName() {
    		return name;
    	}
    	public void setName(String name) {
    		this.name = name;
    	}
    	public int getPrice() {
    		return price;
    	}
    	public void setPrice(int price) {
    		this.price = price;
    	}
    	public String getTaste() {
    		return taste;
    	}
    	public void setTaste(String taste) {
    		this.taste = taste;
    	}
    	public String getMilkYn() {
    		return milkYn;
    	}
    	public void setMilkYn(String milkYn) {
    		this.milkYn = milkYn;
    	}
    	
    	@Override
    	public String toString() {
    		return "name:" + name + " " + "price:" + price + " " + "taste:" + taste + " " + "milkYn:" + milkYn;
    	}
    }
    

     

     위와 같이 setter와 getter를 만들어 사용한다. 다음과 같이 인스턴스 생성 후 값들을 셋팅할 수 있다.

     

    Main.java

    //자바빈 패턴 사용
    CoffeeJavaBean coffee3 = new CoffeeJavaBean();
    coffee3.setName("아메리카노");
    coffee3.setPrice(5000);
    coffee3.setTaste("bitter");
    coffee3.setMilkYn("N");

     

    장점은 다음과 같다.

    • 각 인자의 의미를 정확하게 파악이 가능하게 됨
    • 객체 클래스 내부에 복잡하게 생성자를 만들 필요가 없음

    단점은 다음과 같다.

    • 1회 호출로 객체 생성이 끝나지 않음. setter를 통해 값을 계속 셋팅해줘야 함
    • setter 메서드가 있기 때문에 변경 불가능(immutable)한 클래스를 만들 수 없음(setter를 호출하면 내부 값은 계속 바뀜)

    빌더 패턴

    CoffeeBuilder.java

    package Build;
    
    public class CoffeeBuilder {
    	private final String name;
    	private final int price;
    	private final String taste;
    	private final String milkYn;
    	
    	//객체 생성 전, 값을 셋팅해줄 Builder 내부 클래스
    	public static class Builder {
    		private final String name;
    		private final int price;
    		private final String taste;
    		private final String milkYn; 
    		
    		public Builder name(String name) {
    			this.name = name;
    			return this;
    		}
    		
    		public Builder price(int price) {
    			this.price = price;
    			return this;
    		}
    		
    		public Builder taste(String taste) {
    			this.taste = taste;
    			return this;
    		}
    		
    		public Builder milkYn(String milkYn) {
    			this.milkYn = milkYn;
    			return this;
    		}
    		
    		//값 셋팅이 끝난 후 내부 클래스를 넘겨주어 본 객체에 값을 셋팅해주는 메서드
    		public CoffeeBuilder build() {
    			return new CoffeeBuilder(this);
    		}
    	}
    	
    	//값 셋팅된 Builder 내부 클래스를 본 클래스에 값 셋팅
    	public CoffeeBuilder(Builder builder) {
    		this.name = builder.name;
    		this.price = builder.price;
    		this.taste = builder.taste;
    		this.milkYn = builder.milkYn;
    	}
    	
    	@Override
    	public String toString() {
    		return "name:" + name + " " + "price:" + price + " " + "taste:" + taste + " " + "milkYn:" + milkYn;
    	}
    }

     

     사용은 다음과 같이 한다.

     

    Main.java

    //빌더패턴 사용
    CoffeeBuilder coffeeBuilder = new CoffeeBuilder.Builder()
    				.name("아메리카노")
    				.price(5000)
    				.taste("bitter")
    				.milkYn("N")
    				.build();
    		

     객체 생성 코드를 설명하면, 우선 new CoffeeBuilder.Builder()를 통해 내부에 선언한 static class Builder에 접근한다. 그리고 name("아메리카노").price(5000) 같은 체인 방식으로 값을 셋팅한다. 이런게 가능한 이유는 static class 안의 다음과 같은 return this때문이다.

    public Builder name(String name) {
        this.name = name;
        return this; //셋팅 후, 다시 static class 자기 자신을 return
    }

     

     값을 셋팅 후 자기 자신을 다시 return하여 뒤에 price,taste 등의 셋팅 메서드를 계속 호출할 수 있는 것이다. 모든 셋팅이 끝났으면 마지막에 build()를 호출하는 것을 볼 수 있다. build를 호출하면 셋팅된 static class Builder를 본 객체인 new CoffeeBuilder의 인자로 넣어주어 객체 생성을 할 수 있다.

     

    장점은 다음과 같다.

    • 객체 생성이 되는 순간에 값 setting을 모두 하기 때문에 변경 불가능한(immutable) 객체를 만들 수 있음(변경 불가능하기 때문에 final로 프로퍼티 선언을 함)
    • 한 번에 객체 생성이 가능
    • build()함수로 잘못된 값이 셋팅되었는지 검증 가능

     

    스프링 시큐리티에서의 빌더 패턴 사용

     어디서 많이 본 패턴인가 싶더니 스프링 시큐리티에서 많이 본 방식이다.

     

    SecurityConfig.java

    import lombok.extern.java.Log;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.header.writers.StaticHeadersWriter;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    
    import javax.sql.DataSource;
    
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/user/**").permitAll()
                    .antMatchers("/subadmin/**").hasRole("SUB")
                    .antMatchers("/admin/**").hasRole("ADMIN");
    
            http.formLogin();
            http.exceptionHandling().accessDeniedPage("/accessDenied");
            http.logout().logoutUrl("/logout").invalidateHttpSession(true);
    
        }

     

     위의 시큐리티config의 코드를 보면 http.authorizeRequests()에서 빌더 패턴을 사용하는 것으로 보인다.(jayjay28님 블로그의 예제 코드를 참고하였습니다)

     책 없이 유튜브나 블로그만으로 공부하려니 한계가 있는 것 같다. 전체적으로 디자인 패턴을 학습하면 책으로 깊게 공부해봐야 할 필요성을 느낀다.

     

    출처 : velog.io/@jayjay28/2019-09-04-1109-%EC%9E%91%EC%84%B1%EB%90%A8, johngrib.github.io/wiki/builder-pattern/

    반응형

    댓글