Java Refactoring -2, 혼동되는 생성자 초기화 개선

박태건·2021년 7월 14일
1

리팩토링-자바

목록 보기
2/13
post-thumbnail
post-custom-banner

레거시 코드를 클린 코드로 누구나 쉽게, 리팩토링

위 책을 보면서 정리한 글입니다.

혼동되는 생성장 초기화 개선

복잡하게 존재하는 생성자는 객체 생성과 초기화를 혼란스럽게 할 수 있다.

  • 객체를 생성할 때 매개변수를 전달하여 객체의 필드를 초기화
  • 초기화된 필드는 해당 객체를 사용하는 로직에 영향을 주며, 필드에 대한 초기화는 제대로 이루어져야 한다.
  • 잘못된 객체 초기화는 컴파일러가 잡아내지 못하는 런타임 에러를 발생시킨다.

필드를 초기화하기 위해 호출하는 생성자가 복잡하게 선언되어 있으면 잘못된 객체 생성이 생길 수 있다. 또한, 객체의 필드가 추가 또는 삭제되거나 초기값들의 변경에 대한 요구사항이 생기면 생성자의 수정이 불가피하며, 생성자가 호출된 모든 코드의 수정이 필요하다.

↪ 명확하지 않고 수정에 유연하지 못한 코드는 객체 생성과 초기화를 독립적으로 책임지는 클래스로 분리하여 개선할 필요가 있다.

개선방향

빌더는 객체 생성을 효율적이고 올바르게 관리하는 역할을 할 수 있다.

사용자의 추가 및 정보 수정을 할 때 반드시 초기화되어야 하는 User 객체 필드와 선택적으로 초기화되어야 하는 User 객체 필드를 구분 짓는다.
↪ 필드의 추가 변경이 기존 코드에 영향을 주지 않는 구조로 변경하는데, 이를 위해 빌더 패턴을 활용

빌더 패턴을 활용하여 객체 생성 시에도 정확한 호출이 가능하여 런타임 오류를 방지해주고 요구사항 변경에도 효과적으로 대응 가능

빌더 패턴

빌더 패턴이란

  • 복잡한 객체의 생성 과정과 표현 방법을 분리하여 생성 절차가 동일하더라도 서로 다른 표현 결과를 만들 수 있는 패턴
  • 복잡한 객체의 생성을 빌더에 위임 -> 내부 구조를 몰라도 각각의 요소를 조합 --> 완성된 객체

빌더 패턴의 장점

  • 매개변수에 대한 필수값과 선택값을 명확하게 판단 가능
  • 객체의 필드 변경에 대해서 기존 코드에 영향 없이 대응 가능

빌더 패턴의 단점

  • 기존 생성 방식을 이용한 개발자라면 빌더 구현 자체가 생산 비용
  • 하지만 유지보수를 생각하면 오히려 효율적으로 생각할 수 있다.

빌더 패턴을 언제 이용하는가
1. 생성자 초기화 시 필수 또는 선택해야 하는 값이 존재할 때
2. 생성자 오버로딩이 많아져서 사용이 혼동될 때
3. 매개변수가 4개 이상일 때
4. 오버로딩 방식과 Setter 방식이 혼용되어 사용될 때
5. 초기화한 객체를 불변(만들어진 객체는 수정 불가)으로 만들고 싶을 때

레거시 코드

User 클래스

public class User {
	private String id;			    /** 아이디 : 필수 **/
   	private String password;		/** 비밀번호 : 필수 **/
  	private String name;			/** 이름 : 필수 **/
 	private String email;			/** 이메일 : 선택 **/
   	private String address;			/** 주소 : 선택 **/
 	private String mobile;			/** 전화번호 : 추가 **/
   	private String passport;		/** 여권번호 : 추가 **/
    
  	public User(String id, String password, String name) {
   		this(id, password, name, null);	
   	}
    
   	public User(String id, String password, String name, String email) {
   		this(id, password, name, email, null);	
   	}
    
   	public User(String id, String password, String name, String email, String address) {
             this.id = id;
             this.password = password;
             this.name = name;
             this.email = email;
             this.address = address;
   	}

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public void setPassport(String passport) {
        this.passport = passport;
    }
}

Account 클래스

public class Account {
	private UserService userService = new UserService();
    
    public User createUser(String id, String password, String address) {
        User user = new User(id, password, address);
        return userService.create(user);
    }

    public User updateUser(String id, String password, String email, String name, String address, String mobile, String passport) {
        User user = new User(id, password, name, email, address);
        user.setMobile(mobile);
        user.setPassport(passport);
        return userService.update(user);
    }
}

UserService 클래스

public class UserService {
	public User create(User user) {
        // 중략
        return user;
    }

    public User update(User user) {
        // 중랼
        return user;
    }
}

클라이언트에서 전달받은 매개변수로 User 객체를 생성하고, UserService 클래스의 메서드를 호출합니다. 사실 흔히 보는 스프링에서의 MVC 패턴과 유사하게 구성되어 있다.

위의 코드에서의 문제점은 객체를 생성하기 위해서는 생성자를 호출해야 하지만, 여러 생성자가 오버로드되어 있습니다. 따라서, 어떤 생성자로 어떤 매개변수를 전달하여 인스턴스를 생성할 수 있는지 명확하게 나타내지 못한다.

또, 부가적인 필드의 초기화는 Setter 메서드를 이용하므로, 해당 필드에 대해서는 객체 호출 시점에서 초기화를 보장할 수 없다.
초기화 방식이 일관되지 않아 가독성도 나쁘다.

레거시 코드 개선 과정

레거시 코드의 개선전과 후 클래스 다이어그램 비교

개선 과정은 순서

  1. User 객체를 사용하는 메서드에 대한 테스트 코드 작성
  2. 빌더 패턴 구현
  3. 기존 생성자 초기화 메서드 제거
  4. 메서드 제거 시 컴파일 에러에 대해 빌더를 통한 인스턴스 생성 코드로 교체
  5. 테스트 코드로 동작 검증

AccountTest 클래스


public class AccountTest {
    private Account account = new Account();
    
    @Test
    public void testCreateUser_사용자_계정_생성() {
        //GIVEN

        //WHEN
        User user = account.createUser("id", "password", "name");
        //THEN
        Assert.assertNoNull(user);
    }

    @Test
    public void testUpdateUser_사용자_계정_수정() {
        //GIVEN

        //WHEN
        User user = account.updateUser("id", "password", "name", "email", "address", "mobile", "passort");
        //THEN
        Asser.assertNotNull(user);
    }
}

UserTest 클래스

public class UserTest {
    private User account = new User();
    
    @Test
    public void testUserBuilder_사용자_빌더_객체_생성() {
        //GIVEN

        //WHEN
        User user = new Uesr.Builder("id", "password", "name").build();
        //THEN
        Assert.assertNoNull(user);
    }
}

User 클래스

public class User {
	private String id;			    /** 아이디 : 필수 **/
  	private String password;		/** 비밀번호 : 필수 **/
 	private String name;			/** 이름 : 필수 **/
	private String email;			/** 이메일 : 선택 **/
  	private String address;			/** 주소 : 선택 **/
	private String mobile;			/** 전화번호 : 추가 **/
  	private String passport;		/** 여권번호 : 추가 **/
   
 	public static class Builder {
       private String id;			    /** 아이디 : 필수 **/
       private String password;		/** 비밀번호 : 필수 **/
       private String name;			/** 이름 : 필수 **/
       private String email;			/** 이메일 : 선택 **/
       private String address;			/** 주소 : 선택 **/
       private String mobile;			/** 전화번호 : 추가 **/
       private String passport;		/** 여권번호 : 추가 **/

       // 필수값
       public Builder(String id, String password, String name) {
           this.id = id;
           this.password = password;
           this.name = name;
       }

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

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

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

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

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

   private User(Builder builder) {
       this.id = builder.id;
       this.password = builder.password;
       this.name = builder.name;
       this.email = builder.email;
       this.address = builder.address;
       this.mobile = builder.mobile;
       this.passport = builder.passport;
   }
}
  • User.Builder 생성자를 통해 id, password가 필수값이라는 것을 명확하게 알 수 있다.
  • 입력하려는 변수에 대한 값만 지정하며 마지막 build 메서드를 호출하여 불변 객체를 생성한다.
  • 불변 객체는 객체가 생성된 후에 그 값이 변경되지 않는다.

Getter, Setter를 이용하는 자바빈즈 패턴을 사용하면 여러 명이 진행하는 프로젝트에서 누군가 의도하지 않게 Setter를 이용하여 중간에 값을 변경할 수 있다.
↪ 이런 이유로 해당 필드값의 변경으로 발생하는 문제를 파악하는 데 시간이 많이걸린다. 빌더 패턴을 이용하면 이러한 문제를 사전에 방지 할 수 있다.

개선된 레거시 코드

User 클래스

  • 객체의 생성을 위임받아 처리하는 Builder 클래스 추가
  • 생성자 초기화는 Builder를 통해서만 가능하도록 수정

Account 클래스

  • User 객체의 생성을 빌더에 위임하여 id, password, name이 필수값임이 명확
  • 필요한 요소만 조합해서 User 객체를 생성할 수 있게 수정
  • 값이 추가되어도 기존 로직에는 영향이 없다.

UserService 클래스

  • 사용자 추가나 정보 수정에 대한 내용은 변경사항에 없다.

요약 및 정리

빌더 패턴은 매개변수의 선택값과 필수값이 존재하고, 기존 코드에 영향 없이 매개 변수 변화에 대응해야 할 때 강점이 있다.
빌더 객체를 생성해야 하므로 기존 코드 생성 방식보다 코드와 비용은 늘어날 수 있지만 클라이언트의 유지보수 비용까지 생각한다면 의미있는 작업.

빌더 패턴을 적용할 객체를 생성하기 위한 의식의 흐름

  1. 객체 생성 시 필수 또는 선택적으로 초기화되어야 하는 값이 존재하는지 검토
  2. 생성자에 전달되는 파라미터의 개수가 4개 이상인지 검토
  3. 해당 객체의 필드가 유지보수 과정에서 자주 변경되는지 검토
  4. 1-3의 경우라면 빌더 패턴을 적용하여 개선 시도
profile
노드 리액트 스프링 자바 등 웹개발에 관심이 많은 초보 개발자 입니다
post-custom-banner

0개의 댓글