
객체를 생성하는 정적(static) 메서드로, 일반적인 생성자 대신 사용될 수 있다.
객체 생성을 메서드 내부에서 수행할 수 있어 캡슐화(encapsulation)가 가능하다.
특정 조건에 따라 같은 객체를 재사용하거나 새로운 객체를 반환하는 등의 객체 생명주기 관리가 가능하다.
public static PaymentRequestDto of(String pgCorpName) {
return new PaymentRequestDto(pgCorpName);
}
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
public interface Animal {}
public class Dog implements Animal {}
public class Cat implements Animal {}
public class AnimalFactory {
public static Animal createAnimal(String type) {
return "dog".equals(type) ? new Dog() : new Cat();
}
}
입력 매개변수에 따라 다른 클래스의 인스턴스를 반환하도록 분기할 수 있다. (OCP 준수)
객체 생성을 캡슐화하여 외부로부터 불필요한 노출을 막을 수 있다.
상속이 불가능하다. (private 생성자)
메서드를 찾기 어렵다. -> 명명 규칙을 통해 해결
// from : 매개변수를 "하나" 받아서 해당 타입의 인스턴스를 반환하는 "형변환" 메서드
Date d = Date.from(instant);
// of : "여러" 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 "집계" 메서드
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
// valueOf : from과 of의 더 자세한 버전
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
// instance | getInstance : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
StackWalker luke = StackWalker.getInstance(options);
Calendar instance = Calendar.getInstance();
// create | newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
Object newArray = Array.newInstance(classObject, arrayLen);
// getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. Type은 팩터리 메서드가 반환할 객체의 타입이다.
FileStore fs = Files.getFileStore(path);
// newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. Type은 팩터리 메서드가 반환할 객체의 타입이다.
BufferedReader br = Files.newBufferdReader(path);
// type : getType과 newType의 간결한 버전
List<Complaint> litany = Collections.list(legacyLitany);
public class MemberSignUpRequestDTO {
private Long id;
private String email;
private String password;
private String name;
public MemberSignUpRequestDTO(Long id, String email, String password, String name)
{
this.id = id;
this.email = email;
this.password = password;
this.name = name;
}
}
@Getter
public class PaymentRequestDto {
private final String pgCorpName;
private PaymentRequestDto(String name) {
this.pgCorpName = PgCorp.valueOf(name.toUpperCase()).toString().toLowerCase();
}
public static PaymentRequestDto of(String pgCorpName) {
return new PaymentRequestDto(pgCorpName);
}
}
명확한 네이밍을 통해 가독성을 향상시킨다.
객체의 재사용 및 관리를 용이하게 한다.
OCP 원칙을 준수하는 효율적 코드 작성에 기여한다.
public class ResponsePayment {
public ResponsePayment(String id, String currency, String method, int amount) {
this.id = id;
this.currency = currency;
this.method = method;
this.amount = amount;
}
}
위 코드처럼 매개변수가 많을 경우, 생성자의 가독성이 낮아지고 개발자가 실수(human error)할 가능성이 증가한다. 이러한 한계점을 Builder 패턴으로 극복할 수 있다.
public class ResponsePayment {
private final String id;
private final String currency;
private final String method;
private final int amount;
private ResponsePayment(Builder builder) {
this.id = builder.id;
this.currency = builder.currency;
this.method = builder.method;
this.amount = builder.amount;
}
public static class Builder {
private String id;
private String currency;
private String method;
private int amount;
public Builder id(String id) { this.id = id; return this; }
public Builder currency(String currency) { this.currency = currency; return this; }
public Builder method(String method) { this.method = method; return this; }
public Builder amount(int amount) { this.amount = amount; return this; }
public ResponsePayment build() {
return new ResponsePayment(this);
}
}
}
@Getter
@Builder
public class Payment {
private String id;
private String currency;
private String method;
private int amount;
}
Constructor: 매개변수가 적고 필수적인 값만으로 객체를 생성할 때 사용한다.
Static Factory Method: 객체 생성 로직이 복잡하거나, 객체 생성을 캡슐화해야 할 때 사용한다.
Builder 패턴 직접 구현: 매개변수가 많고 선택적인 매개변수가 존재할 때 가독성과 유지보수성을 높이기 위해 사용한다.
Lombok의 @Builder 사용: 코드를 매우 간결하게 하고 싶을 때 사용한다. 다만 Lombok 라이브러리에 대한 의존성이 생기므로 trade-off를 잘 따져서 사용해야 할 것이다.
가독성을 향상시키고 유지보수를 용이하게 한다.
불필요한 코드를 제거한다.
선택적 매개변수를 처리할 수 있도록 지원한다.
"불변식"을 지키는 객체다. 객체 생성 후 내부 상태를 변경할 수 없으므로 안정성이 보장된다.
예: “List의 경우 size의 크기는 반드시 0 이상이어야 하고 한 순간이라도 음수 값이 될 수 없다.”라는 조건식이 List.size()의 불변식이다.
클래스를 final로 선언하여 "상속"을 막는다.
클래스의 모든 필드를 final로 선언하여 "변경"을 막는다.
모든 필드를 private으로 선언하여 "외부에서의 직접 접근"을 막는다.
Setter 메서드를 제공하지 않는다.
만약 필드가 가변 객체를 참조한다면, 해당 객체의 복사본을 반환하도록 한다.
단순성: 상태가 변하지 않으므로 클라이언트는 개발자가 예측한 대로만 동작한다.
자유롭게 공유 가능: 동기화 문제 없이 객체를 공유할 수 있다.
실패 원자성 보장 (멱등성): 연산 실패 시 객체의 상태가 변하지 않아 예측 가능하고 안전한 코드를 작성할 수 있다.
안정성과 예측 가능성을 보장한다.
병렬 프로그래밍에서 강점을 가진다.

함수를 "값"으로 취급할 수 있게 되었으며(= 1st class citizen), 코드 가독성이 크게 개선되었다.
Predicate<Apple> filterByRed = (Apple a) -> "RED".equals(a.getColor());
Function<String, Integer> getLength1 = (String s) -> s.length();
Function<String, Integer> getLength2 = String::length;
filterBy(apples, (Apple apple) -> "RED".equals(apple.getColor()));
filterBy(apples, (a) -> a.getWeight() > 15);

실행하려는 메서드를 참조해서 매개변수와 리턴타입을 알아내어, 람다식의 불필요한 선언부를 생략할 수 있는 문법이다.

.map((String s) -> s.length());
.map(String::length); // 선언부 생략
(Apple a , Apple b) -> a.getWeight().compare(b.getWeight());
(a, b) -> a.getWeight().compare(b.getWeight()); // type 생략해도 컴파일러가 추론 가능
.comparing(apple -> apple.getWeight().getValue())
.comparing(Apple::getValue)
코드를 간결하고 가독성 좋게 만들어준다.
함수형 프로그래밍 패러다임을 쉽게 적용할 수 있도록 지원한다.


데이터 처리 연산을 지원하도록 source data에서 추출된 "요소들의 연속(sequence of elements)"을 의미한다.
선언형 프로그래밍 (추상화): 달성하고자 하는 "목적"을 선언하여 프로그래밍한다. Stream API를 사용하기 때문에 API 내부에 명령 코드가 미리 작성되어 있어 가능한 것이다.
public static int imperativeSum(List<Integer> nums) { // 명령형
int sum = 0;
for (int i = 0; i < nums.size(); i++) {
sum += nums.get(i);
}
return sum;
}
public static int declarativeSum(List<Integer> nums) { // 선언형
return nums.stream().reduce(0, Integer::sum);
}
method chaining: 여러 중간연산을 연결해 데이터 처리 파이프라인을 만들 수 있다. 이때 각 메서드들은 중간 연산이 처리된 새로운 "스트림"을 반환한다. (= "pipelining")
Lazy Evaluation: 중간 연산들은 "최종 연산 도달시" 한꺼번에 지연 실행되어 성능을 향상시킨다. 즉 최종 연산이 시작되어야 비로소 컬렉션의 요소가 하나씩 중간 스트림에서 처리가 되고 최종 처리까지 오게 된다.
"내부" 반복자: 병렬 처리의 경우 외부 반복은 "개발자(클라이언트 코드)"가 lock이나 synchronized 등으로 직접 처리해야 하지만, Stream API 같은 내부 반복은 "API"가 알아서 처리하므로 부담을 덜 수 있다. (참고 - Iterator vs Stream API)

가독성: 코드의 의도를 명확하게 드러내어 가독성을 높인다.
추상화: 데이터 처리 로직을 추상화하여 코드의 복잡성을 줄인다.
불변성: 스트림 연산은 원본 데이터를 변경하지 않고 새로운 스트림을 생성하므로 데이터 불변성을 유지할 수 있다.
성능: 경우에 따라 전통적인 반복문보다 성능이 떨어질 수 있다.
디버깅: 스트림 파이프라인의 중간 결과를 확인하기 어려워 디버깅이 까다로울 수 있다.
public void calculateTotalAmount() {
this.totalPrice = this.items.stream().map(item -> item.calculateAmount())
.reduce(0, Integer::sum);
}

private void orderCancelBy(int itemIdx) {
this.items.stream().filter(orderItem -> orderItem.getItemIdx() == itemIdx)
.forEach(item -> item.update(OrderStatus.ORDER_CANCELLED));
}

선언형 프로그래밍을 가능하게 한다.
method chaining을 통해 간결하고 가독성 높은 코드를 작성할 수 있도록 지원한다.
내부 반복자를 통해 병렬 처리를 쉽게 구현할 수 있도록 돕는다.
에러의 근원(NPE): NullPointerException은 Java에서 가장 흔하게 발생하는 에러 중 하나이다.
가독성 저하: null 처리를 위한 if-else 문으로 가독성이 떨어진다.
아무 의미가 없다: null은 아무 의미도 표현하지 않는다. 특히 정적 타입 언어에서 값이 없음을 표현하는 방법으로는 적절하지 않다.
Java 철학에 위배: 자바는 개발자로부터 모든 포인터를 숨겼다. 하지만 예외가 있는데 그것이 바로 "null 포인터"다.
type 시스템에 구멍을 만든다: null은 무형식이며 정보를 포함하고 있지 않으므로 모든 참조 타입에 null을 할당할 수 있다. 하지만 이런식으로 null이 할당되기 시작하면서 시스템의 다른부분으로 null이 퍼졌을 때 애초에 null이 어떤 의미로 사용되었는지 알 수 없다.
위 문제를 해결하고 null을 올바르게 처리하기 위해 Java 8부터 java.util.Optional<T> 클래스가 추가되었다.
T 타입의 값을 캡슐화하는 클래스이다. 값이 존재하는 경우 그 값을 감싸지만, 값이 존재하지 않는 경우 null이 아닌 Optional.Empty 메서드로 Optional을 반환한다.
// 1. Empty 값
Optional.empty();
// 2. 값이 null이 될 수 "없는" 경우
Optional<String> pgCorpName = Optional.of(paymentRequestDto.getPgCorpName());
// 3. 값이 null이 될 수 "있는" 경우
Optional<String> pgCorpName = Optional.ofNullable(paymentRequestDto.getPgCorpName());
isPresent(): 값이 존재하는지 여부를 반환한다.
T get(): 값을 반환한다. 값이 없는 경우 NoSuchElementException을 발생시킨다.
orElse(T other): 값이 없는 경우 other 값을 반환한다.
orElseGet(Supplier<? extends T> other): 값이 없는 경우 Supplier를 통해 생성된 값을 반환한다.
@Override
public CardPayment findById(String paymentKey) {
return jpaCardPaymentRepository.findById(paymentKey)
.orElseThrow(() -> new NoSuchElementException(String.format("CardPayment with
key '%s' not found", paymentKey)));
}
public void verifyOrderIsCompleted(UUID orderId) throws IllegalArgumentException {
Optional<Order> orderInfo = orderRepository.findByIdV2(orderId);
if(orderInfo.isPresent()) {
OrderStatus status = orderInfo.get().getStatus();
if (!status.equals(OrderStatus.ORDER_COMPLETED))
throw new IllegalArgumentException("Order is not completed || Order is
already paymented");
}
}
null 처리를 명시적으로 드러내고, NullPointerException 발생 가능성을 줄인다.
코드 가독성을 향상시키고, 안전하고 견고한 코드 작성에 기여한다.
기본형(Primitive Type)과 래퍼 클래스(Wrapper Class) 간의 자동 타입변환 기능을 의미한다.
개발자는 타입 변환 로직에 신경 쓰지 않아도 되며, 코드를 간결하고 직관적으로 작성할 수 있게 된다.
Java Collection Framework는 "객체"만 저장 가능하므로 래퍼 클래스를 사용해야 하기 때문에, 이와 연동이 용이하다.
기본형 데이터를 래퍼 클래스로 변환하여 스트림 연산에 활용하거나, 반대로 스트림 결과를 기본형으로 쉽게 변환할 수 있다.
성능 문제
개발자의 가독성과 편의성 면에서는 좋지만, Auto Boxing/Unboxing 과정에서 추가적인 객체 생성과 GC 동작으로 인해 성능 문제를 일으킬 수 있다. (따라서 불필요한 boxing 처리는 피하는 것이 좋다)
NullPointerException(NPE)
래퍼 클래스는 객체이므로 null 값을 가질 수 있다. 따라서 언박싱 시 null 참조를 처리하지 않으면 NPE가 발생할 수 있다.
long startTime, endTime;
startTime = System.currentTimeMillis();
ArrayList<Integer> indexs = new ArrayList<>();
for(int i = 0; i < 1000000; i++){
indexs.add(i); // int -> Integer (박싱)
}
int sum = indexs.stream().reduce(0, Integer::sum); // Integer -> int (언박싱)
endTime = System.currentTimeMillis();
System.out.println("Time taken: " + (endTime - startTime) + "ms");
System.out.println("Sum: " + sum);
IntStream.Builder indexBuilder = IntStream.builder();
long startTime, endTime;
startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
indexBuilder.add(i);
}
IntStream indexs = indexBuilder.build();
int sum = indexs.sum();
endTime = System.currentTimeMillis();
System.out.println("Time taken: " + (endTime - startTime) + "ms");
System.out.println("Sum: " + sum);