본문 바로가기
[JAVA]/JAVA 기본

정적 팩토리 메서드의 특징과 사용법을 예제로 이해하기

by 팡펑퐁 2023. 6. 20.
728x90

정적 팩토리 메서드(Static Factory Method)

  • 개발자가 직접 구성한 Static Method를 사용하여 생성자를 호출하고, 그 생성자로 객체를 생성하는 디자인 패턴이다.

 

 

백문이 불여일타이니 직접 상황을 만들어보고 코드를 짜보자.

  • 클라이언트로부터 요구사항을 전달받았다.
  • Member에 대한 정보를 받아 DB에 저장하는 간단한 프로젝트를 만들라는 것이다.
  • 아주 간단해 보이니 바로 만들어보자.

 

 

MemeberController

@RequestMapping
@RestController
public class MemberController {
    private final MemberService memberService;

    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
    @PostMapping("/members")
    public ResponseEntity createMember(@RequestBody MemberDto.Post postDto) {
        MemberDto.Response savedMember = memberService.createMember(postDto);


        return new ResponseEntity<>(savedMember, HttpStatus.CREATED);
    }
}
  • MemberController에 요청 메시지가 JSON 형식을 들어오면, JSON 데이터를 postDto라는 자바 Dto 객체로 변환한다.
  • 그다음 Service 클래스로 Dto 객체를 넘겨 비즈니스 로직을 처리하게 된다.

 

MemberService

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public MemberDto.Response createMember(MemberDto.Post postDto) {

        Member member = 
        	new Member(postDto.name, postDto.age, postDto.country, LocalDateTime.now());

        Member savedMember = memberRepository.save(member);


        MemberDto.Response response = MemberDto.Response.builder()
                .id(savedMember.getMemberId())
                .name(savedMember.getName())
                .age(savedMember.getAge())
                .country(savedMember.getCountry())
                .createdAt(savedMember.getCreatedAt()).build();

        return response;
    }
}
  • Controller로부터 넘겨받은 Dto객체를 Member 엔티티 객체로 매핑(변경)한다.
    • postDto -> entity 매핑에는 생성자 메서드를 사용했다.
  • 매핑한 엔티티 객체를 memberRepository.save(member)를 통해 데이터베이스에 저장한다.
  • 이후에 저장된 객체를 다시 Response라는 Dto 객체로 매핑하고 응답메시지를 보낸다.
    • entity -> reponseDto 매핑에는 빌더 패턴을 사용했다.

 

 

중간 정리

  • 클라이언트로부터 받은 요청메시지(JSON 데이터) -> 컨트롤러단(Dto 객체로 바뀜) -> 서비스단에서 비즈니스 로직 처리 후(PostDto -> Entity -> ResponseDto) -> 컨트롤러단 리턴(ResponseEntity 객체로 변환) -> 클라이언트로 응답 메시지 전송

 

 

MemberDto

public class MemberDto {
    @Getter
    public static class Post {
        public String name;
        public int age;
        public String country;
    }

    @Builder
    public static class Response {
        public Long id;
        public String name;
        public int age;
        public String country;
        public LocalDateTime createdAt;
    }
}
  • entity에서 response DTO로 변환하기 위해 @Builder를 사용했다.

 

Member(Entity)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

    private String name;

    private int age;

    private String country;

    private LocalDateTime createdAt;

    public Member(String name, int age, String country, LocalDateTime createdAt) {
        this.name = name;
        this.age = age;
        this.country = country;
        this.createdAt = createdAt;
    }
}
  • postDto에서 entity로 변환하기 위해 생성자 메서드를 사용했다.

 

 

클라이언트의 요구사항 변화

  • 악덕 클라이언트가 기껏 다 만들어놨더니 요구사항 변경을 원하고 있다.
  • 클라이언트의 변경된 요구사항은 다음과 같다.
  • 국적은 South Korea를 고정시킨다.
  • 요청 데이터에 country 값이 없다면 South Korea로, 있다면 해당 국적을 넣어 저장하라.

 

 

현재 코드의 문제점

public MemberDto.Response createMember(MemberDto.Post postDto) {
		
        // postDto -> entity
        Member member = 
        	new Member(postDto.name, postDto.age, postDto.country, LocalDateTime.now());
        
        Member savedMember = memberRepository.save(member);

		
        // entity -> ResponseDto
        MemberDto.Response response = MemberDto.Response.builder()
                .id(savedMember.getMemberId())
                .name(savedMember.getName())
                .age(savedMember.getAge())
                .country(savedMember.getCountry())
                .createdAt(savedMember.getCreatedAt()).build();

        return response;
    }
  • 변경된 요구사항을 이행하기 위해서는 postDto -> entity 매핑에 사용할 생성자 메서드가 요청 메시지에 따라 달라져야 한다.

 

 

중간 정리

<요구 사항의 변화로 country가 포함되지 않은 요청메시지가 올 수 있다.>

  • 기존에는 요청 메시지를 받아 그대로 postDto -> entity로 매핑해 주면 됐다.
  • 그런데, 요구사항의 변화로 요청 메시지에 county라는 필드가 포함되어 있는지의 여부에 따라 매핑에 사용할 메서드를 변경해야 한다.
  • 기존의 매핑 메서드를 그대로 사용하게 되면 요청 메시지에 country가 없을 경우 응답으로 null이 출력되기 때문이다.
  • 방법은 여러 가지가 있지만 생성자 오버로딩을 사용해 보자.

 

 

생성자 오버로딩

public Member(String name, int age, String country, LocalDateTime createdAt) {
    this.name = name;
    this.age = age;
    this.country = country;
    this.createdAt = createdAt;
}

//추가
public Member(String name, int age,LocalDateTime createdAt) {
    this.name = name;
    this.age = age;
    this.country = "South Korea";
    this.createdAt = createdAt;
}
  • 생성자 오버로딩이란 생성자 메서드를 오버로딩하는 것이다.
  • 오버로딩은 같은 메서드명을 사용하되 파라미터의 데이터 타입이나 수를 다르게 하는 것을 말한다.
  • 여기에서는 파라미터로 country를 받지 않는 생성자 메서드를 추가하고 country를 South Korea로 고정했다.

 

 

MemberService

public MemberDto.Response createMember(MemberDto.Post postDto) {

    Member member;
    if (postDto.country == null) {
        member = new Member(postDto.name, postDto.age, LocalDateTime.now());
    } else {
        member = new Member(postDto.name, postDto.age, postDto.country, LocalDateTime.now());
    }

    Member savedMember = memberRepository.save(member);


    MemberDto.Response response = MemberDto.Response.builder()
            .id(savedMember.getMemberId())
            .name(savedMember.getName())
            .age(savedMember.getAge())
            .country(savedMember.getCountry())
            .createdAt(savedMember.getCreatedAt()).build();

    return response;
}
  • 그리고 Service 단의 로직에 조건문을 추가해 요청 메시지에 country가 있는 경우와 없는 경우를 나눠 다른 생성자 메서드를 통해 Member 객체로 매핑(생성)하게 변경했다.

 

 

결과

  • country의 포함 여부에 따라 사용하는 생성자 메서드가 달라지게 되었다.
  • 그런데, 지금 이 코드는 매우 간단한 코드이기 때문에 이 정도 수준에서 끝낼 수 있지만 매우 복잡한 코드라면? 그리고 여러 명이 협업하는 코드라면? 과연 생성자 메서드가 최선의 방법일까?

 

 

생성자 대신 정적 팩토리 메서드를 고려해 보기 위해 정적 팩토리 메서드의 정의에 대해 다시 살펴보자.

  • 정적 팩토리 메서드란 개발자가 직접 구성한 Static Method를 사용하여 생성자를 호출하고, 그 생성자로 객체를 생성하는 디자인 패턴이다.
  • 이 말의 의미를 정적 메서드의 특징을 통해 파악해 보자.

 

 

생성자 오버로딩

public Member(String name, int age, String country, LocalDateTime createdAt) {
    this.name = name;
    this.age = age;
    this.country = country;
    this.createdAt = createdAt;
}

//추가
public Member(String name, int age,LocalDateTime createdAt) {
    this.name = name;
    this.age = age;
    this.country = "South Korea";
    this.createdAt = createdAt;
}
  • country 인자를 받냐 안받냐의 차이만 가지고 있다.

 

정적 팩토리 메서드

private Member(String name, int age, String country, LocalDateTime createdAt) {
    this.name = name;
    this.age = age;
    this.country = country;
    this.createdAt = createdAt;
}

public static Member anotherCountriesOf(String name, int age, String country, LocalDateTime createdAt) {
    return new Member(name, age, country, createdAt);
}

public static Member southKoreaOf(String name, int age, LocalDateTime createdAt) {
    return new Member(name, age, "South Korea", createdAt);
}
  • 메서드명과 인자를 통해 country를 받지 않으면 South Korea를 디폴트로 설정하고, country를 받으면 해당 국적으로 객체를 생성한다는 것을 유추할 수 있다.

 

 

정적 팩토리 메서드의 특징

1. 생성 목적을 알 수 있는 메서드 네이밍이 가능하다.

  • 위의 예시에서 조건에 따라 다른 객체를 생성하기 위해 생성자 오버로딩을 사용했다. 
  • 생성자 메서드를 사용하여 객체를 생성할 때 단점은?
    • new 키워드로 생성자를 생성하기 위해서는 개발자가 해당 생성자 인자의 순서와 내부 구조를 알고 있어야 한다.
    • new 키워드로 객체를 생성하는 방법은 그저 인자의 타입, 개수, 순서에 맞춰 입력할 뿐이지 생성 목적을 알 수 있는 어떠한 단서도 제공하지 않는다.
    • 혼자 개발할 때는 크게 상관이 없을 수 있으나, 여러 명이서 협업하여 개발할 때에는 좋지 않을 수 있다.
  • 정적 팩토리 메서드의 경우 위와 같이 정적 메서드를 만들고 메서드 네이밍에서 생성 목적이나 역할을 알 수 있도록 한다면 설계 의도를 타 개발자에게 쉽게 전달할 수 있게 된다.
    • 가독성 있는 설계는 협업에 도움을 줄 수 있다.

 

2. 인스턴스에 대한 통제 및 관리가 가능하다.

  • 정적 팩토리 메서드를 구성할 때는 생성자를 private 접근 제어자로 하고 정적 메서드를 이용하여 new 키워드로 만든 객체를 리턴한다.
    • 메서드를 통해 한 단계를 거쳐 간접적으로 객체를 생성하기 때문에 객체 생성에 대한 통제 및 관리가 가능하다.

 

3. 인자에 따라 다른 객체를 반환하도록 나눌 수 있다.

interface Person {
    public static Person getAge(int age) {
        if(age > 20) {
            return new 성인();
        }

        if(age > 8) {
            return new 학생();
        }

        return new 아기();
    }
}

 

4. 캡슐화가 가능하다.

  • 생성자 메서드를 사용하면 외부에 내부 구현을 드러내야 한다.
  • 정적 팩토리 메서드에서는 구현부를 숨겨 캡슐화 및 정보 은닉이 가능하다.

 

정적 팩토리 메서드 적용 후 Service 코드

public MemberDto.Response createMember(MemberDto.Post postDto) {

    Member member;
    if (postDto.country == null) {
        member = Member.southKoreaOf(postDto.name, postDto.age, LocalDateTime.now());
    } else {
        member = Member.anotherCountriesOf(postDto.name, postDto.age, postDto.country, LocalDateTime.now());
    }

    Member savedMember = memberRepository.save(member);
  • 메서드 명으로 인해 명확히 역할이 드러나며, 무분별한 객체 생성을 막을 수 있게 되었다.

 

 

정적 팩토리 메서드 네이밍 규칙

  • 다른 정적 메서드와 구분하기 위해 네이밍 컨벤션이 존재한다.
  • from : 하나의 인자를 받아서 객체를 생성하는 메서드이다.
  • of : 여러 개의 인자를 받아서 객체를 생성하는 메서드이다.

 

 

 

 

참고

https://inpa.tistory.com/entry/GOF-💠-정적-팩토리-메서드-생성자-대신-사용하자

728x90