[이펙티브 자바] 객체의 생성과 파괴

도모·2022년 12월 14일
0

이펙티브자바

목록 보기
1/1
post-thumbnail

정리한 내용 중 잘못된 내용이 있을 수 있습니다. 잘못된 내용이 있다면 꼭 알려주세요.

1.객체의 생성

1-1. Contructor와 Static Factory Method의 비교

//Laptop Class
public class Laptop {
	private String modelName;
    private String company;
}
//Constructor
public Laptop(String modelName, String company) {
	this.modelName = modelName;
    this.company = company;
}
//이름을 가진 Static Factory Method
public static Laptop ofModelNameAndCompany(String modelName, String company) {
	Laptop laptop = new Laptop();
    laptop.company = company;
    laptop.modelName = mpdelName;
    return laptop;
}

Lombok에 @AllargsConstructor가 있는데도 불구하고 Static Factory Mathod를 사용하면
언제 편할까?

public class LaptopForm {
	private String name;
    private String corp;
}
@PostMapping("/add")
public LaptopDto addLaptop(@RequestBody LaptopForm laptopform) {
	//생략
}

위와 같이 Controller에 addLaptop이라는 메서드가 있다라고 가정할 때,
LaptopForm과 같이 Request를 받는 객체를 지정하여 사용하는 경우가 많다.
이럴 때 아래와 같이 Laptop Class에 Static Factory Method를 사용하여 convert 해줄 수 있다.

public class Laptop {
	private String modelName;
    private String company;
    
    public static Laptop from(LaptopForm laptopForm) {
    	Laptop laptop = new Laptop();
        laptop.modelName = laptopForm.getName();
        laptop.company = laptopForm.getCorp();
        return laptop;
    }
}

또한 Response를 응답 할 때도 아래와 같이 Static Factory Method를 사용하여 convert 해줄 수 있다.

public class LaptopDto {
	private String modelName;
    private String companyName;
    
    public static LaptopDto from(Laptop laptop) {
    	LaptopDto laptopDto = new LaptopDto();
        laptopDto.modelName = laptop.getModelName();
        laptopDto.companyName = laptop.getCompany();
        return laptopDto;
    }
}

그래서 Static Factory Method를 사용했을 때 장 단점을 정리해보자면

장점

  • 이름을 가질 수 있다.
  • Simple하고 명확하게 사용할 수 있다.
  • 인스턴스를 매번 생성할 필요는 없다.
    Flyweight pattern - Collection Object
    Singleton pattern - Single Object
    (Singleton의 경우 한 객체, Flyweight의 경우 multiple한 객체를 다룬다.)

단점

  • Static Factory Method만 제공하면 Constructor가 없을 수 있어 상속받은 Class를 만들 수 없다.
  • 프로그래머에게 인지가 잘 되지 않을 수 있다.

1-2. 많은 parameter가 있는 Constructor는 Builder를 고려하라

public class NutritionFacts {
	private final int servingSize;		// ml, 1회 제공량 (필수)
    private final int servings;			// 회, 총 n회 제공량 (필수)
    private final int calories;			// 1회 제공량당 (need)
    private final int fat;				// g/1회 제공량 (option)
    private final int solum;			// mg/1회 제공량 (option)
    private final int carbohydrate;		// g/1회 제공량 (option)
}

Pattern 1 : 여러 개의 Constructor를 둔다.
Pattern 2 : Java beans Pattern (Setter)

Builder 를 직접 만들었을 때

Builder를 Lombok을 통해서만 사용해 왔으면 해당 코드를 한번은 주의깊게 살필 필요가 있다.

public class NutritionFacts {
	private final int servingSize;		// ml, 1회 제공량 (필수)
    private final int servings;			// 회, 총 n회 제공량 (필수)
    private final int calories;			// 1회 제공량당 (need)
    
    public static class Builder {
    	private final int servingSize;
        private final int servings;
        private int calories = 0;
        public Builder(int servingSize, int servings) {
        	this.servingSize = servingSize;
            this.servings = servings;
        }
        public Builder calories(int val) {
        	calories = val;
        	return this;
        }
        public NutritionFacts build() {
        	return new NutritionFacts(this);
        }
    }
}

Lombok을 통하면 훨씬 더 Simple해 질 수 있다.

@Data
@Builder(builderMethodName = "hiddenBuilder")
public class NutritionFacts {
	private final int servingSize;		// ml, 1회 제공량 (필수)
    private final int servings;			// 회, 총 n회 제공량 (필수)
    @Builder.Defatult private final int calories = 0;			// 1회 제공량당 (need)
    @Builder.Defatult private final int fat = 0;				// g/1회 제공량 (option)
    @Builder.Defatult private final int solum = 0;			// mg/1회 제공량 (option)
    @Builder.Defatult private final int carbohydrate = 0;		// g/1회 제공량 (option)
    
    public static NutritionFacts builder(int servingSize, int servings) {
    	return hiddenBuilder()
        	.servingSize(servingSize)
            .servings(servings);
    }
}

그래서 Builder를 사용했을 때 장 단점을 정리해보자면

장점

  • 상속받은 Class의 Builder가 정의한 build 메서드가 상위 메서드의 타입을 return 하는 것이 아닌 자신의 타입을 return한다.

단점

  • Builder를 항상 만들어야 하기 때문에 생성 비용이 무조건 생긴다.
  • 점층적 생성자 패턴 (Argument를 여러개 가진 Constructor)보다 장황하여 적은 갯수의 Parameter일 경우 오히려 좋지 않을 수 있다.

1-3. privateConstructor나 enumType으로 Singleton임을 보장하라.

//대표적인 Singleton Pattern
public class Speaker {
	public static final Speaker INSTANCE = new Speaker();
    privete Speaker() {}
    
    public static Speaker getInstance() {
    	return INSTANCE;
    }
}
public class Speaker {
	private static Speaker instance;
    private Speaker() {}
    public static synchronized Speaker getInstance() {
    	if (instance == null) {
        	instance = new Speaker();
        }
        return instance;
    }
}

Enum Type으로 Singleton Pattern을 사용할 수 있다.

public enum Speaker {
	INSTANCE;
    private String message;
    public Speaker getInstance() {
    	return INSTANCE;
    }
    public void setMessage(String message) {
    	this.message = message;
    }
    public String getMessage() {
    	return message;
    }
}

실행코드

Speaker speaker1 = Speaker.INSTANCE.getInstance();
speaker1.setMessage("안내 방송 중입니다.");
Speaker speaker2 = Speaker.INSTANCE;
System.out.println(speaker1.getMessage()); // 안내 방송 중입니다.
System.out.println(speaker2.getMessage()); // 안내 방송 중입니다.

하지만 오히려 혼란이 생겨 현업에서 enum으로 잘 사용하지는 않는다고 한다.

정리하자면
Singleton pattern에 대해 알아보았고, 항상 잘 숙지해 두자.
각 방법의 차이점에 대해 인지해 두고 이를 상황에 맞게 선택해 사용할 수 있도록 하자.
Enum 방식은 팀원들과 함께 고려해 보자.

1-4.Instance화를 막으려면 private constructor를 사용하라

public class PatternUtil {
	private static final String PATTERN =
      "\\b[A-Z0-9._%+-]+@{A-Z0-9.-]+\\.[A-Z\{2,}\\b";
      
    private PatternUtil() {}
    
    public static boolean isEmailValid(String email) {
    	return email.matches(PATTERN);
    }
    
    public String getPattern() {
    	return PATTERN;
    }
}

private PatternUtil() {}가 없다고 가정하고,
어느 한 개발자가 PATTERN String 값 자체가 필요해지는 순간이 온다라고 가정하였을 때,
private으로 되어있는 PATTERN을 public으로 변경하려고 하다가, 코드에 수정 없이
public String getPattern() 메소드를 정의하게 된다.

만약에 private PatternUtil() {} 을 선언해준다면 PatternUtil 객체를 생성할 수 없다는 것을
알게되고 public static String getPatter()으로 변경하여 가져올거다.

사람은 믿지 말자.
과거의 나😃 : 에이 누가 필요도 없는 Instance를 생성하겠어? 굳이 막지 않아도 알아서 다들 잘 지킬 수 있겠지!
현재의 나😂 : 바쁘다 바빠, (습관적으로 Instance 생성)
미래의 나😈 : (리팩토링중) 누가 필요도 없는 Instance를 생성한 거야? 어?? 나네?

나와 팀원의 실수를 줄이기 위해 조금 귀찮더라도 생성자를 막자.

정리하자면
누군가(나를 포함한)를 믿지 못할 상황이 있고, Human error를 방지하기 위해
private constructor를 권장한다. (많은 노력이 필요한 것이 아니다.)

일반적으로 Util class처럼 관용적으로 Instance화를 하지 않을 거라고 모두가 이미 알고 있는 경우는 생략하기도 한다.

1-5.Resource를 직접 명시하지 말고, Dependency Injection을 사용하라

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
...

상식 쌓기 차원에서 @Configuration 의 명세를 가져와 봤습니다.
@Configuration은 @Component를 포함하고 있고,
@Component가 Singleton이기에 자연스럽게 @Configuration 또한 Singleton !

변수들을 Config class 내부에 직접 정의하여 사용하는것은 좋지 않다.
그 이유는 여러 자원을 활용해야 할 경우에 적합하지 않고,
환경(dev, local)별로 다른 값들이 들어가야 할 때 적절하지 않다.

아래에 안 좋은 예를 살펴보자

@Configuration
public class Config {
	private static final String ADDRESS = "서울시 강남구";
    public Config() {
    	this.ADDRESS = "서울시 강남구";
    }
}

그렇다면 좋은 예를 살펴보자
application.yml에 등록된 정보

config:
  address: '서울시 강남구'
@Configuration
public class Config {
	@Value("${config.address}")
    private String address;
}

Constructor Injection의 경우 Test, flexibility를 높일 수 있다.

고정해 높은 값은 비해 훨씬 유연해진 class (pattern Injection 가능)
Test Code 작성 시 Injection하기도 편리하다.

public class PhonePatternChecker {
	private final String pattern;
    
    public PhonePatternChecker(String pattern) {
    	this.pattern = pattern;
    }
    public boolean isValid(String phone) { ... }
}

Config Class를 생각없이 사용하고 있지는 않았는지
Singleton을 이해하고 있는지
Dependency Injection (DI)를 잘 이해하고 있는지

생각해보시면 좋을 것 같다.

profile
안녕하세요. 백엔드 엔지니어 도모입니다.

0개의 댓글