[Effective Java] - 메서드 시그니처를 신중히 설계하라
Item 51 : 메서드 시그니처를 신중히 설계하라
들어가며
API 설계에서 메서드 시그니처는 개발자가 매일 마주하는 인터페이스다. 잘 설계된 메서드 시그니처는 코드의 가독성을 높이고 오류 가능성을 줄이며, API의 사용성을 크게 향상시킨다. 반대로 부주의하게 설계된 시그니처는 혼란을 야기하고 버그의 온상이 되며, 한번 공개되면 영원히 지원해야 하는 부담이 된다.
이 아이템에서는 메서드 시그니처를 설계할 때 고려해야 할 핵심 원칙들을 살펴본다. 메서드 이름 짓기, 매개변수 개수 제한, 매개변수 타입 선택 등 실무에서 즉시 적용할 수 있는 구체적인 지침을 다룬다.
1. 메서드 이름을 신중히 지어라
메서드 이름은 해당 메서드가 무엇을 하는지 명확히 전달해야 한다. 이름만 보고도 메서드의 동작을 예측할 수 있어야 한다.
표준 명명 규칙을 따르라
Java 커뮤니티에는 오랜 시간 동안 확립된 명명 규칙이 있다. 이를 따르면 다른 개발자들이 코드를 이해하기 쉬워진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// 좋은 예: 명확하고 표준적인 이름
public class UserRepository {
public User findById(Long id) { ... }
public List<User> findAll() { ... }
public void save(User user) { ... }
public void delete(User user) { ... }
}
// 나쁜 예: 불명확하고 비표준적인 이름
public class UserRepository {
public User get(Long id) { ... } // 너무 일반적
public void store(User user) { ... } // 의미가 명확하지 않음
}
일관성을 유지하라
같은 패키지나 라이브러리 내에서는 일관된 명명 패턴 을 유지해야 한다. 같은 동작을 하는 메서드는 같은 단어를 사용하라.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 좋은 예: 일관된 명명 패턴
public interface Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
boolean add(E e);
boolean remove(Object o);
}
// 나쁜 예: 일관성 없는 명명 패턴
public interface InconsistentCollection<E> {
int getSize(); // size()와 혼용
boolean empty(); // isEmpty()와 혼용
boolean has(Object o); // contains()와 혼용
Iterator<E> getIterator(); // iterator()와 혼용
}
Java Collections Framework를 보면 size(), isEmpty(), contains() 같은 메서드가 모든 컬렉션에서 동일한 이름으로 제공된다. 이런 일관성 덕분에 개발자들은 새로운 컬렉션 타입을 접해도 빠르게 적응할 수 있다.
긴 이름을 피하고, 너무 짧진 않게 작성
메서드 이름은 명확성과 간결성 사이의 균형 을 유지해야 한다. IDE의 자동완성 기능이 있다고 해서 지나치게 긴 이름을 사용하는 것은 좋지 않다.
1
2
3
4
5
6
7
8
9
10
11
// 적절한 길이
public User findByEmail(String email) { ... }
public void updatePassword(String newPassword) { ... }
// 너무 긴 이름
public User findUserByEmailAddressFromDatabase(String email) { ... }
public void updateUserPasswordInDatabaseWithValidation(String newPassword) { ... }
// 너무 짧은 이름
public User find(String e) { ... } // 의미 불명확
public void update(String p) { ... } // 의미 불명확
2. 편의 메서드를 너무 많이 만들지 마라
클래스나 인터페이스는 자신의 역할을 충실히 수행하는 메서드만 제공 해야 한다. 메서드가 너무 많으면 이를 구현하고, 문서화하고, 테스트하고, 유지보수하기 어려워진다.
핵심 기능에 집중하라
모든 조합 가능한 메서드를 제공하려 하지 말고, 기본적인 기능만 제공 하라. 나머지는 사용자가 조합해서 만들 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 좋은 예: 핵심 기능만 제공
public class Rectangle {
private final double width;
private final double height;
public double getWidth() { return width; }
public double getHeight() { return height; }
public double getArea() { return width * height; }
public double getPerimeter() { return 2 * (width + height); }
}
// 나쁜 예: 너무 많은 편의 메서드
public class Rectangle {
private final double width;
private final double height;
public double getWidth() { return width; }
public double getHeight() { return height; }
public double getArea() { return width * height; }
public double getPerimeter() { return 2 * (width + height); }
public double getHalfArea() { return getArea() / 2; }
public double getDoubleArea() { return getArea() * 2; }
public double getAreaInSquareFeet() { return getArea() * 10.764; }
public double getHalfPerimeter() { return getPerimeter() / 2; }
public double getDiagonal() { return Math.sqrt(width * width + height * height); }
// ... 너무 많은 메서드들
}
두 번째 예제의 getHalfArea(), getDoubleArea() 같은 메서드는 사용자가 getArea() / 2, getArea() * 2로 쉽게 구현할 수 있다. 자주 사용되는 경우에만 편의 메서드를 제공하라.
자주 사용되는 경우만 추가하라
편의 메서드를 추가할지 고민된다면 다음을 체크해보자
- 이 메서드가 정말 자주 사용되는가?
- 이 메서드가 없으면 사용자가 복잡한 코드를 작성해야 하는가?
- 이 메서드가 성능상 이점을 제공하는가?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// String 클래스의 좋은 예
public final class String {
// 핵심 메서드
public int length() { ... }
public char charAt(int index) { ... }
// 자주 사용되는 편의 메서드
public boolean isEmpty() {
return length() == 0; // 단순하지만 매우 자주 사용됨
}
public boolean isBlank() {
// 구현이 복잡하고 자주 사용되므로 제공
return indexOfNonWhitespace() == length();
}
}
3. 매개변수 목록은 짧게 유지하라
매개변수는 4개 이하 가 이상적이다. 매개변수가 많아질수록 메서드를 사용하기 어려워지고, 실수할 가능성이 높아진다. 특히 같은 타입의 매개변수 가 여러 개 있으면 순서를 헷갈리기 쉽다.
문제가 되는 긴 매개변수 목록
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 나쁜 예: 매개변수가 너무 많고 같은 타입이 반복됨
public void createUser(
String firstName,
String lastName,
String email,
String phoneNumber,
String address,
String city,
String state,
String zipCode,
int age,
boolean isActive
) {
// 구현
}
// 사용 시 순서를 헷갈리기 쉽다
createUser(
"John",
"Doe",
"555-1234", // 실수! email과 phoneNumber 순서가 바뀜
"john@example.com",
"123 Main St",
"Springfield",
"IL",
"62701",
30,
true
);
위 예제의 문제는 타입 시스템이 순서 오류를 잡아내지 못한다 는 것이다. email과 phoneNumber 모두 String이므로 컴파일러는 순서가 바뀌어도 오류를 내지 않는다.
해결 방법 1: 여러 메서드로 쪼개라
메서드가 여러 기능을 수행한다면 각 기능을 담당하는 메서드로 분리 하라.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 좋은 예: 기능별로 메서드 분리
public class UserService {
public User createBasicUser(String firstName, String lastName, String email) {
// 기본 사용자 생성
}
public void updateContactInfo(User user, String phoneNumber, String email) {
// 연락처 정보 업데이트
}
public void updateAddress(User user, Address address) {
// 주소 정보 업데이트
}
}
List 인터페이스의 subList 메서드는 좋은 예시다.

리스트의 부분을 다루는 모든 연산(찾기, 정렬, 복사 등)에 대해 각각의 메서드를 제공하는 대신, 부분 리스트를 반환하는 메서드 하나 만 제공한다. 사용자는 이를 일반 리스트처럼 다루면 된다.
1
2
3
4
5
List<String> list = new ArrayList<>(List.of("a", "b", "c", "d", "e"));
// subList 하나로 다양한 연산 가능
list.subList(1, 4).clear(); // 부분 삭제
list.subList(0, 2).sort(Comparator.naturalOrder()); // 부분 정렬
해결 방법 2: 매개변수 객체를 만들어라
여러 매개변수를 묶어서 하나의 클래스로 만들면 매개변수 개수를 줄이고, 코드의 가독성도 높일 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 좋은 예: 매개변수 객체 사용
public class Address {
private final String street;
private final String city;
private final String state;
private final String zipCode;
public Address(String street, String city, String state, String zipCode) {
this.street = Objects.requireNonNull(street);
this.city = Objects.requireNonNull(city);
this.state = Objects.requireNonNull(state);
this.zipCode = Objects.requireNonNull(zipCode);
}
// getters...
}
public class ContactInfo {
private final String email;
private final String phoneNumber;
public ContactInfo(String email, String phoneNumber) {
this.email = Objects.requireNonNull(email);
this.phoneNumber = phoneNumber; // phoneNumber는 선택적
}
// getters...
}
public class UserService {
public User createUser(
String firstName,
String lastName,
ContactInfo contactInfo,
Address address,
int age
) {
// 구현
}
}
// 사용
User user = userService.createUser(
"John",
"Doe",
new ContactInfo("john@example.com", "555-1234"),
new Address("123 Main St", "Springfield", "IL", "62701"),
30
);
매개변수 객체를 사용하면 타입 안전성 도 높아진다. ContactInfo와 Address는 서로 다른 타입이므로 순서를 바꾸면 컴파일 오류가 발생한다.
해결 방법 3: 빌더 패턴을 사용하라
매개변수가 많고 대부분 선택적이라면 빌더 패턴 을 고려하라.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 좋은 예: 빌더 패턴
public class User {
private final String firstName; // 필수
private final String lastName; // 필수
private final String email; // 필수
private final String phoneNumber; // 선택
private final Address address; // 선택
private final int age; // 선택
private final boolean isActive; // 선택
private User(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.email = builder.email;
this.phoneNumber = builder.phoneNumber;
this.address = builder.address;
this.age = builder.age;
this.isActive = builder.isActive;
}
public static class Builder {
// 필수 매개변수
private final String firstName;
private final String lastName;
private final String email;
// 선택 매개변수 - 기본값으로 초기화
private String phoneNumber = "";
private Address address = null;
private int age = 0;
private boolean isActive = true;
public Builder(String firstName, String lastName, String email) {
this.firstName = Objects.requireNonNull(firstName);
this.lastName = Objects.requireNonNull(lastName);
this.email = Objects.requireNonNull(email);
}
public Builder phoneNumber(String val) {
phoneNumber = val;
return this;
}
public Builder address(Address val) {
address = val;
return this;
}
public Builder age(int val) {
age = val;
return this;
}
public Builder isActive(boolean val) {
isActive = val;
return this;
}
public User build() {
return new User(this);
}
}
}
// 사용: 가독성이 뛰어남
User user = new User.Builder("John", "Doe", "john@example.com")
.phoneNumber("555-1234")
.age(30)
.isActive(true)
.build();
// 선택 매개변수는 생략 가능
User simpleUser = new User.Builder("Jane", "Smith", "jane@example.com")
.build();
4. 매개변수 타입으로는 클래스보다 인터페이스가 낫다
매개변수 타입을 정할 때는 구체적인 클래스보다 인터페이스를 사용 하라. 이렇게 하면 클라이언트가 더 유연하게 메서드를 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// 나쁜 예: 구체 클래스를 매개변수로 받음
public void processUsers(ArrayList<User> users) {
for (User user : users) {
process(user);
}
}
// 사용 시 ArrayList만 전달 가능
ArrayList<User> arrayList = new ArrayList<>();
processUsers(arrayList); // OK
LinkedList<User> linkedList = new LinkedList<>();
processUsers(linkedList); // 컴파일 오류!
위 코드는 ArrayList만 받을 수 있다. 클라이언트가 LinkedList나 다른 List 구현체를 사용하고 있다면 억지로 ArrayList로 변환해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 좋은 예: 인터페이스를 매개변수로 받음
public void processUsers(List<User> users) {
for (User user : users) {
process(user);
}
}
// 모든 List 구현체 사용 가능
ArrayList<User> arrayList = new ArrayList<>();
processUsers(arrayList); // OK
LinkedList<User> linkedList = new LinkedList<>();
processUsers(linkedList); // OK
Vector<User> vector = new Vector<>();
processUsers(vector); // OK
더 상위 인터페이스를 사용할 수 있다면 그것이 더 좋다. 순회만 하면 되는 경우라면 List 대신 Collection을, 순회만 하면 되고 중복 제거가 필요하지 않다면 Iterable을 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
// 더 나은 예: 최소한의 인터페이스 사용
public void processUsers(Collection<User> users) {
for (User user : users) {
process(user);
}
}
// Set, Queue 등도 사용 가능
Set<User> userSet = new HashSet<>();
processUsers(userSet); // OK
Queue<User> userQueue = new LinkedList<>();
processUsers(userQueue); // OK
실제 사례: Collections 유틸리티 클래스
Collections 클래스의 메서드들을 보면 이 원칙이 잘 적용되어 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Collections {
// List만 정렬 가능하므로 List를 받음
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
// 어떤 컬렉션이든 최댓값을 찾을 수 있으므로 Collection을 받음
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) {
// 구현
}
// 순회만 하면 되므로 Iterable을 받으면 더 좋을 수 있음
public static <T> boolean addAll(Collection<? super T> c, T... elements) {
// 구현
}
}
5. boolean보다는 열거 타입을 사용하라
boolean 매개변수는 메서드 호출 시 의미가 불명확 하다. 코드를 읽는 사람은 true나 false가 무엇을 의미하는지 API 문서를 봐야 알 수 있다.
1
2
3
4
5
6
7
8
9
10
// 나쁜 예: boolean 매개변수
public class Thermometer {
public double getTemperature(boolean celsius) {
// celsius가 true면 섭씨, false면 화씨
}
}
// 사용: 의미가 불명확
double temp1 = thermometer.getTemperature(true); // true가 뭔지 모름
double temp2 = thermometer.getTemperature(false); // false가 뭔지 모름
호출 코드만 봐서는 true와 false가 무엇을 의미하는지 전혀 알 수 없다. API 문서를 보거나 메서드 정의로 이동해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 좋은 예: 열거 타입 사용
public class Thermometer {
public enum TemperatureUnit {
CELSIUS, FAHRENHEIT
}
public double getTemperature(TemperatureUnit unit) {
// 구현
}
}
// 사용: 의미가 명확
double temp1 = thermometer.getTemperature(TemperatureUnit.CELSIUS);
double temp2 = thermometer.getTemperature(TemperatureUnit.FAHRENHEIT);
열거 타입을 사용하면 코드 자체가 문서 역할 을 한다. IDE의 자동완성 기능도 사용할 수 있어 편리하다.
여러 boolean 매개변수의 문제
boolean 매개변수가 여러 개면 문제는 더 심각해진다.
1
2
3
4
5
6
7
8
9
10
// 매우 나쁜 예: 여러 boolean 매개변수
public class TextFormatter {
public String format(String text, boolean bold, boolean italic, boolean underline) {
// 구현
}
}
// 사용: 완전히 불명확
String result = formatter.format("Hello", true, false, true);
// 무엇이 true이고 무엇이 false인가?
이런 경우 빌더 패턴이나 매개변수 객체 를 사용하는 것이 좋다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 좋은 예: 매개변수 객체 사용
public class TextStyle {
private final boolean bold;
private final boolean italic;
private final boolean underline;
private TextStyle(Builder builder) {
this.bold = builder.bold;
this.italic = builder.italic;
this.underline = builder.underline;
}
public static class Builder {
private boolean bold = false;
private boolean italic = false;
private boolean underline = false;
public Builder bold() {
this.bold = true;
return this;
}
public Builder italic() {
this.italic = true;
return this;
}
public Builder underline() {
this.underline = true;
return this;
}
public TextStyle build() {
return new TextStyle(this);
}
}
public boolean isBold() { return bold; }
public boolean isItalic() { return italic; }
public boolean isUnderline() { return underline; }
}
public class TextFormatter {
public String format(String text, TextStyle style) {
// 구현
}
}
// 사용: 명확하고 읽기 쉬움
TextStyle style = new TextStyle.Builder()
.bold()
.underline()
.build();
String result = formatter.format("Hello", style);
실제 사례: EnumSet
Java의 EnumSet은 비트 필드를 열거 타입으로 대체한 좋은 예다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 나쁜 예: 비트 필드와 boolean의 조합
public class Text {
public static final int BOLD = 1 << 0;
public static final int ITALIC = 1 << 1;
public static final int UNDERLINE = 1 << 2;
public void applyStyles(int styles) {
// 구현
}
}
text.applyStyles(Text.BOLD | Text.UNDERLINE); // 여전히 불명확
// 좋은 예: EnumSet 사용
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE }
public void applyStyles(Set<Style> styles) {
// 구현
}
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.UNDERLINE)); // 명확
마치며
메서드 시그니처 설계는 API 사용성의 핵심이다. 다음 원칙들을 기억하라:
메서드 이름 은 명확하고 표준 규칙을 따르며 일관성을 유지해야 한다. 편의 메서드 는 정말 필요한 경우에만 추가하고, 핵심 기능에 집중하라. 매개변수는 4개 이하 로 유지하고, 많아지면 메서드 분리, 매개변수 객체, 빌더 패턴을 고려하라.
매개변수 타입 으로는 구체 클래스보다 인터페이스를 사용하고, 가능한 한 상위 인터페이스를 선택하라. boolean 매개변수 는 피하고 열거 타입이나 매개변수 객체로 대체하라.
이런 원칙들을 따르면 사용하기 쉽고, 이해하기 쉬우며, 오류가 적은 API를 설계할 수 있다. 좋은 API는 처음 접하는 개발자도 쉽게 사용할 수 있고, 숙련된 개발자는 더욱 생산적으로 만든다. 메서드 시그니처는 작은 결정의 연속이지만, 이런 작은 결정들이 모여 API의 품질을 결정한다.
References
- 이펙티브 자바 3/E
