메서드 체이닝과 문자열
this 반환이 만드는 체인의 실행 순서를 추적하고 학습 기록 문장 빌더·파일명 분석·검색·역순 변환을 안전한 문자열 도구로 완성합니다.
메서드가 다음 호출 대상 객체를 반환하면 점을 이어 여러 연산을 한 표현식으로 작성할 수 있습니다. StringBuilder의 append가 대표적입니다. 체이닝은 호출 순서를 읽기 쉽게 만들 수 있지만 반환 계약이 끊기거나 중간 상태가 숨겨지면 디버깅을 어렵게 합니다.
체이닝 중간의 null 실패
다음 빌더의 topic은 this를 반환하지만 minutes는 실수로 null을 반환합니다. 문법은 컴파일되며 실제 실행에서 그 다음 build 호출이 실패합니다.
public final class BrokenFluentChainFailure {
public static void main(String[] args) {
String line = new StudyLineBuilder()
.topic("string")
.minutes(45)
.build();
System.out.println(line);
}
private static final class StudyLineBuilder {
private String topic;
private int minutes;
StudyLineBuilder topic(String value) {
topic = value;
return this;
}
StudyLineBuilder minutes(int value) {
minutes = value;
return null;
}
String build() {
return topic + ":" + minutes;
}
}
}Exception in thread "main" java.lang.NullPointerException체인 가능한 메서드는 정상 경로에서 항상 문서화된 대상 객체를 반환해야 합니다. 현재 객체를 계속 구성한다면 this, 불변 변환이라면 새 값, 처리 단계가 바뀐다면 다음 단계 타입을 반환합니다. null은 체인을 끊는 정상 신호로 사용하지 않습니다.
this 반환과 객체 동일성
각 호출이 필드를 바꾸고 this를 돌려주면 모든 표현식이 하나의 빌더 인스턴스를 사용합니다. 별도 변수로 풀어 쓰면 실행 원리를 확인할 수 있습니다.
public final class ValueAdderChaining {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
ValueAdder afterOne = adder.add(1);
ValueAdder afterTwo = afterOne.add(2);
ValueAdder afterThree = afterTwo.add(3);
System.out.println("same=" + (adder == afterThree));
System.out.println("expanded=" + afterThree.value());
int chained = new ValueAdder().add(1).add(2).add(3).value();
System.out.println("chained=" + chained);
}
private static final class ValueAdder {
private int value;
ValueAdder add(int amount) {
value += amount;
return this;
}
int value() { return value; }
}
}same=true
expanded=6
chained=6afterOne, afterTwo, afterThree은 각 시점의 스냅숏이 아니라 같은 객체의 별칭입니다. 나중에 하나를 수정하면 모든 변수를 통해 새 값이 보입니다. 체인 빌더는 한 식이나 한 메서드 안에서만 사용하고 중간 참조를 장기간 보관하지 않는 편이 안전합니다.
학습 기록 문장 조립
build에서 필수 값을 검사하면 완성되지 않은 결과가 외부로 나가는 것을 막을 수 있습니다. 체인 메서드는 입력 즉시 단순 범위를 검증하고, 필드 간 관계는 최종 생성 시 확인합니다.
public final class StudyLineFluentBuilder {
public static void main(String[] args) {
String line = new StudyLineBuilder()
.topic("method chaining")
.minutes(50)
.completed(true)
.build();
System.out.println(line);
}
private static final class StudyLineBuilder {
private String topic;
private int minutes;
private boolean completed;
StudyLineBuilder topic(String value) {
if (value == null || value.isBlank()) throw new IllegalArgumentException("topic");
topic = value.strip();
return this;
}
StudyLineBuilder minutes(int value) {
if (value <= 0) throw new IllegalArgumentException("minutes");
minutes = value;
return this;
}
StudyLineBuilder completed(boolean value) {
completed = value;
return this;
}
String build() {
if (topic == null) throw new IllegalStateException("topic missing");
if (minutes == 0) throw new IllegalStateException("minutes missing");
return new StringBuilder()
.append(topic)
.append(':')
.append(minutes)
.append(':')
.append(completed ? "done" : "open")
.toString();
}
}
}method chaining:50:done선택 매개변수가 많고 이름으로 의미를 보여 줘야 할 때 빌더가 유용합니다. 필수 값이 두세 개뿐이면 생성자가 더 짧고 불완전한 중간 객체를 만들지 않습니다. 체이닝을 쓰고 싶다는 이유만으로 단순 값 객체 앞에 빌더를 추가하지 않습니다.
불변 객체의 연쇄 변환
String의 strip().toLowerCase().replace(...)는 각 단계가 새 String 값을 돌려주므로 가능한 체인입니다.
가변 this 체인과 달리 중간 값이 서로 다른 인스턴스일 수 있습니다.
호출자는 반환 타입과 불변 계약을 보고 의미를 판단합니다.
import java.util.Locale;
public final class ImmutableTopicPipeline {
public static void main(String[] args) {
String raw = " Java Object Model ";
String slug = raw
.strip()
.toLowerCase(Locale.ROOT)
.replace(' ', '-');
System.out.println("raw='" + raw + "'");
System.out.println("slug=" + slug);
}
}raw=' Java Object Model '
slug=java-object-model중간 결과를 조사해야 한다면 체인을 여러 줄의 이름 있는 변수로 풀어냅니다. 예외가 날 수 있는 파싱이나 서로 다른 업무 단계를 한 체인에 과도하게 묶으면 실패 위치와 정책이 숨겨집니다.
문자열 문제와 메서드 선택
문자열 연습은 반복문부터 작성하기보다 입력과 원하는 결과의 관계를 분류합니다.
- 특정 문자의 길이와 위치: length, indexOf, lastIndexOf
- 파일 이름과 확장자 분리: 마지막 구분자 위치 + substring
- 검색어 등장 횟수: indexOf를 시작 위치와 함께 반복
- 양끝 공백 정리: strip
- 특정 단어 교체: replace
- 단어 순서 결합: split 후 join
- 역순 문자: StringBuilder.reverse
검색어 개수를 셀 때 다음 탐색 시작 위치를 한 글자 뒤로 옮기면 겹치는 검색도 셉니다. 검색어 길이만큼 옮기면 겹치지 않는 등장만 셉니다. 요구사항이 어느 쪽인지 예제로 확정해야 합니다.
public final class StringProblemToolkit {
public static void main(String[] args) {
System.out.println("count=" + count("hello java, hello string", "hello"));
System.out.println("base=" + baseName("study.notes.txt"));
System.out.println("ext=" + extension("study.notes.txt"));
System.out.println("reverse=" + new StringBuilder("object").reverse());
}
private static int count(String source, String keyword) {
if (keyword.isEmpty()) throw new IllegalArgumentException("keyword");
int count = 0;
int from = 0;
while (true) {
int found = source.indexOf(keyword, from);
if (found < 0) return count;
count++;
from = found + keyword.length();
}
}
private static String baseName(String fileName) {
int dot = fileName.lastIndexOf('.');
return dot <= 0 ? fileName : fileName.substring(0, dot);
}
private static String extension(String fileName) {
int dot = fileName.lastIndexOf('.');
return dot <= 0 || dot == fileName.length() - 1 ? "" : fileName.substring(dot + 1);
}
}count=2
base=study.notes
ext=txt
reverse=tcejbo빈 검색어를 허용하면 indexOf가 같은 위치를 계속 반환해 무한 루프가 될 수 있으므로 경계에서 거부합니다. 파일명 규칙도 시작 점을 확장자 구분자로 볼지 업무 요구에 맞춰 조정합니다.
메서드 체이닝 선택 기준
각 단계의 반환 타입이 명확하고 왼쪽에서 오른쪽으로 자연스럽게 읽히며 실패 정책이 단순할 때 체인을 사용합니다. 중간 값을 재사용하거나 조건 분기가 필요하거나 한 줄이 여러 책임을 섞으면 이름 있는 문장으로 나눕니다. 체인의 길이보다 개념 단계 수가 판단 기준입니다.
가변 빌더는 한 소유자에게 제한하고, build 결과는 불변 값으로 반환하며, build 이후 재사용을 허용할지 정합니다. 사용 후 빌더를 계속 바꾸면 이미 만든 String은 불변이라 영향을 받지 않지만 새 build 결과는 달라집니다.
연습 문제
TitleBuilder에 prefix, topic, suffix를 차례로 설정하고 [JAVA] OBJECT - DONE을 만드세요.
각 설정 메서드는 this를 반환하고 build는 topic 누락을 거부합니다.
해설 보기
public final class TitleBuilderExercise {
public static void main(String[] args) {
String title = new TitleBuilder()
.prefix("java")
.topic("object")
.suffix("done")
.build();
System.out.println(title);
}
private static final class TitleBuilder {
private String prefix = "";
private String topic;
private String suffix = "";
TitleBuilder prefix(String value) {
prefix = "[" + value.strip().toUpperCase() + "] ";
return this;
}
TitleBuilder topic(String value) {
topic = value.strip().toUpperCase();
return this;
}
TitleBuilder suffix(String value) {
suffix = " - " + value.strip().toUpperCase();
return this;
}
String build() {
if (topic == null || topic.isBlank()) throw new IllegalStateException("topic missing");
return prefix + topic + suffix;
}
}
}[JAVA] OBJECT - DONE설정 메서드는 같은 builder를 이어 주고 build가 최종 String 경계를 만듭니다. 필수 topic만 생성자에서 받고 선택 요소만 체인으로 두는 대안도 가능합니다.