adSense 900*70


Builder Pattern JAVA - Pattern

Builder Pattern

Builder pattern이 생각날때 마다 함께 떠오르는 예전 기억이 있다.
예전 모 은행 프로젝트를 할때 였다. 관리자 싸이트의 게시판 관련 코드를 수정할 일이 있었는데,
그때 코드를 보고 와우~ 이건 뭐지? 하며 놀란 적이 기억이 난다.
아래와 비슷한 코드였던 걸로 기억이 난다.
생성자에 4개이상의 인자를 넘겨 주고 생성자도 하나가 아닌 여러개를 인자의 갯수 별로, 즉, tele scoping 생성자 패턴을 사용한 코드였다.
실제 코드는 아래의 예제보다 훨씬 더 길었던 것으로 기억된다.
(사실 tele scoping을 의도하고 한건지는 잘 모르겠다. 그 이유는 final인 속성 필드가 하나도 없었기 때문이다. 즉, 인스턴스를 생성시점 -생성자를 호출하는 시점-에 모든 인자를 넘겨주면서 기본 속성마져도 final로 해놓지 않는다는 건 생성자를 굳이 쓸 필요가 없다는 것이다.)

import java.text.SimpleDateFormat;
import java.util.Date;

public class BoardArticle {
private String boardName;
private String title;
private String author;
private String content;
private String createDate;

public BoardArticle(String boardName, String title, String author, String content) {
this(boardName, title, author, content, new SimpleDateFormat("yyyymmdd").format(new Date(System.currentTimeMillis())));
}

public BoardArticle(String boardName, String title, String author, String content, String createDate) {
this.boardName = boardName;
this.title = title;
this.author = author;
this.content = content;
this.createDate = createDate;
}

public String getBoardName() {
return boardName;
}

public void setBoardName(String boardName) {
this.boardName = boardName;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public String getCreateDate() {
return createDate;
}

public void setCreateDate(String createDate) {
this.createDate = createDate;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((author == null) ? 0 : author.hashCode());
result = prime * result
+ ((boardName == null) ? 0 : boardName.hashCode());
result = prime * result + ((content == null) ? 0 : content.hashCode());
result = prime * result
+ ((createDate == null) ? 0 : createDate.hashCode());
result = prime * result + ((title == null) ? 0 : title.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BoardArticle other = (BoardArticle) obj;
if (author == null) {
if (other.author != null)
return false;
} else if (!author.equals(other.author))
return false;
if (boardName == null) {
if (other.boardName != null)
return false;
} else if (!boardName.equals(other.boardName))
return false;
if (content == null) {
if (other.content != null)
return false;
} else if (!content.equals(other.content))
return false;
if (createDate == null) {
if (other.createDate != null)
return false;
} else if (!createDate.equals(other.createDate))
return false;
if (title == null) {
if (other.title != null)
return false;
} else if (!title.equals(other.title))
return false;
return true;
}

@Override
public String toString() {
return "BoardArticle [boardName=" + boardName + ", title=" + title
+ ", author=" + author + ", content=" + content
+ ", createDate=" + createDate + "]";
}

}

정말 게시물 인스턴스를 하나 만들기위해 이렇게 해야 하는건지…
이런 tele scoping 생성자 패턴을 사용하면 인자의 갯수가 많아 질 수록 생성자를 관리하기도 힘들 뿐더러, 실제 호출하는 쪽(인스턴스를 생성하는)에서 인자 값들을 유심히, 주의 깊게 살펴봐야 한다. 그렇지 않으면 titile에 author가 들어가거나, author에 content가 들어가버리는 경우가
발생할 여지가 크다고 생각한다.

그럼 이걸 어떻게 개선을 해야 할까? 생성자로 넘기는 인자를 줄이고 모두 setter로 처리할까?
그렇게 하는 것도 하나의 방법일 수 있다.
하지만 여러번의 setter메서드 호출로 인스턴스의 생성 및 속성을 설정하기 때문에 인스턴스가 일관된 상태를 유지 할 수 없고, thread에서의 안전성을 보장 할 수 없다. 말이 thread의 안전성을 보장할 수 없다는 거지, 실제 문제가 생기면 어디서, 왜 문제가 발생 했는지 굉장히 찾기 힘들다.

따라서 setter를 통해서 인스턴스를 완성해가는 방법 또한 별로 추천하지 않는다.

그럼 제목처럼 이제 Builder라는 pattern을 살펴보자.

import java.text.SimpleDateFormat;
import java.util.Date;

public class BoardArticle {
private final String boardName;
private final String title;
private final String author;
private final String content;
private final String createDate;

private BoardArticle(Builder builder) {
this.boardName = builder.boardName;
this.title = builder.title;
this.author = builder.author;
this.content = builder.content;
this.createDate = builder.createDate;
}

public String getBoardName() {
return boardName;
}

public String getTitle() {
return title;
}

public String getAuthor() {
return author;
}

public String getContent() {
return content;
}

public String getCreateDate() {
return createDate;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((author == null) ? 0 : author.hashCode());
result = prime * result
+ ((boardName == null) ? 0 : boardName.hashCode());
result = prime * result + ((content == null) ? 0 : content.hashCode());
result = prime * result
+ ((createDate == null) ? 0 : createDate.hashCode());
result = prime * result + ((title == null) ? 0 : title.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BoardArticle other = (BoardArticle) obj;
if (author == null) {
if (other.author != null)
return false;
} else if (!author.equals(other.author))
return false;
if (boardName == null) {
if (other.boardName != null)
return false;
} else if (!boardName.equals(other.boardName))
return false;
if (content == null) {
if (other.content != null)
return false;
} else if (!content.equals(other.content))
return false;
if (createDate == null) {
if (other.createDate != null)
return false;
} else if (!createDate.equals(other.createDate))
return false;
if (title == null) {
if (other.title != null)
return false;
} else if (!title.equals(other.title))
return false;
return true;
}

@Override
public String toString() {
return "BoardArticle [boardName=" + boardName + ", title=" + title
+ ", author=" + author + ", content=" + content
+ ", createDate=" + createDate + "]";
}


public static class Builder{
private String boardName;
private String title;
private String author;
private String content;
private String createDate = new SimpleDateFormat("yyyymmdd").format(new Date(System.currentTimeMillis()));

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

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

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

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

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

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

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

실제 코드는 더 길어 지지만 인스턴스를 생성하는 부분을 비교해 보자. 주석으로 처리된 부분이 tele scoping 생성자를 사용하여 생성하는 부분이며 그 아래의 부분이 Builder 패턴을 사용하여
인스턴스를 생성한 코드이다. 실제 그렇게 큰 차이는 느끼지 못할 수도 있겠지만 각 인자 값의 길이가 길이 질 경우를 생각해보면 Builder pattern을 사용하여 인스턴스를 생성하는 것이 더 깔끔해 보인다. 그리고 이렇게 Builder를 통해 생성할 경우, thread safe하다. 왜냐면 Builder를 통해
생성한 인자의 각 속성은 모두 final이기 때문이다.
또한 각 속성을 설정할 경우, 각 속성마다 validation 로직이 들어가게 되면 생성자에 validation관련 코드까지 넣게 되면 코드는 더 지저분 하게 될 수 있다.
반면 Builder를 사용할 경우는 각 속성에 해당 하는 메서드 안에서 validation처리를 하면 되므로 코드를 깔끔하게 유지할 수 있을 것으로 생각된다.

따라서 객체의 인스턴스를 생성할때, 인자가 4개 이상 될 경우, 혹은 4개가 당장은 되지 않더라도
늘어날 가능성이 있는 경우에는 Builder pattern을 사용하여 인스턴스를 생성하는 것을 반드시 고려해보도록 하자.

import static org.junit.Assert.*;

import org.junit.Test;

public class BoardArticleBuilderTest {

@Test
public void test(){
//BoardArticle BoardArticle = new BoardArticle("boardName", "title", "author", "content", "createDate"); // old code from tele scoping constructor pattern
BoardArticle boardArticle = BoardArticle.Builder.newInstance()
.boardName("boardName")
.author("author")
.content("content")
.createDate("20150701")
.title("title")
.build();

assertNotNull(boardArticle);
}
}

덧글

댓글 입력 영역


side adsense

adSense 900*70