2025.03.08 - [Core Java/Chapter 4] - 코어자바(Core Java) 12판 Chapter 4 리뷰 : 4.2 표준 라이브러리 클래스 사용하기
4.2 표준 라이브러리 클래스 사용하기
4.2 표준 라이브러리 클래스 사용하기(Using Predefined Classes)자바에서는 미리 정의된 표준 라이브러리 클래스(Predefined Classes)를 제공하며, 이를 통해 개발자는 직접 구현하지 않아도 다양한 기능을
choosla.tistory.com
[ Employee 예제로 배우는 나만의 클래스 설계 방법 ]
4.3 자신만의 클래스 정의하기(Defining Your Own Classes)
좀 더 복잡한 프로그램을 만들기 위해서는 중추가 되는 클래스와 같은 요소들을 설계하는 방법을 알아야 한다. 이러한 클래스들은 대개 main 메소드가 없으며, 자체적인 인스턴스 필드와 메소드를 가진다. 프로그램을 완성할 때 여러 클래스를 조합하고 그 중 한 클래스에 main 메소드를 두어 전체 실행 흐름을 제어한다.
4.3.1 Employee 클래스
클래스의 가장 기본적인 형태는 다음과 같다.
class 클래스이름 {
필드1;
필드2;
...
생성자1;
생성자2;
...
메소드1;
메소드2;
...
}
이 형식에 맞춰 회사에서 사용되는 급여 시스템에 대한 가장 단순한 버전의 Employee 클래스를 작성해보자.
class Employee {
// 인스턴스 필드
private String name;
private double salary;
private LocalDate hireDay;
// 생성자
public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
// 메소드
public String getName() {
return name;
}
// 더 많은 메소드들
...
}
프로그램에서는 Employee 객체로 채워진 배열을 다음과 같이 생성할 수 있다.
Employee[] staff = new Employee[3];
staff[0] = new Employee("A", ... );
staff[1] = new Employee("B", ... );
staff[2] = new Employee("C", ... );
그리고 for-each 문을 사용하여 각 사원의 이름을 출력할 수 있다.
for (Employee e : staff) {
System.out.println(e.getName() + " 사원님");
}
(컴파일에 관한 추가 정보는 나중에 다루기로 한다.)
4.3.3 Employee 클래스 분석하기
클래스를 분석하기 전에 Employee 클래스의 전체 코드는 아래와 같다.
import java.time.*;
// 이 프로그램은 Employee(직원) 클래스를 테스트하는 코드이다.
public class EmployeeTest {
public static void main(String[] args) {
// 직원 정보를 저장할 배열 생성 (3명의 직원)
Employee[] staff = new Employee[3];
// 직원 배열에 Employee 객체를 생성하여 추가
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
// 모든 직원의 급여를 5% 인상
for (Employee e : staff) {
e.raiseSalary(5);
}
// 직원 정보 출력
for (Employee e : staff) {
System.out.println("이름=" + e.getName() +
", 급여=" + e.getSalary() +
", 입사일=" + e.getHireDay());
}
}
}
// Employee 클래스: 직원 정보를 저장하고 관리
class Employee {
private String name; // 직원 이름
private double salary; // 급여
private LocalDate hireDay; // 입사일
// 생성자 - 직원 정보 초기화
public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
// 직원 이름 반환
public String getName() {
return name;
}
// 직원 급여 반환
public double getSalary() {
return salary;
}
// 직원 입사일 반환
public LocalDate getHireDay() {
return hireDay;
}
// 급여를 특정 퍼센트만큼 인상
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
}
우선, Employee 클래스는 하나의 생성자와 네 개의 메소드로 구성되어 있다.
public Employee(String n, double s, int year, int month, int day)
public String getName()
public double getSalary()
public LocalDate getHireDay()
public void raiseSalary(double byPercent)
모든 메소드에는 public 키워드가 붙어 있어, 어떤 클래스에서든 호출이 가능하다.
또한, Employee 객체의 내부 데이터는 다음과 같은 세 개의 인스턴스 필드에 저장된다.
private String name;
private double salary;
private LocalDate hireDay;
private 키워드는 오직 Employee 클래스 내부의 메소드만 이 필드들에 접근할 수 있음을 의미한다. 외부에서는 읽거나 쓸 수 없다.
주의: 인스턴스 필드를 public으로 선언하는 것은 캡슐화를 완전히 깨뜨릴 수 있으므로 강력히 권장하지 않는다.
4.3.4 생성자에 대해서
Employee 클래스의 생성자를 보면 다음과 같다.
public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
코드에서 보듯이, 생성자의 이름은 클래스 이름과 동일하며, 객체가 생성될 때 인스턴스 필드를 초기화한다.
예를 들어, 다음과 같이 Employee 객체를 생성할 경우
new Employee("A", 100000, 1999, 1, 21)
생성자는 아래와 같이 인스턴스 필드를 초기화한다.
name = "A"
salary = 100000
hireDay = LocalDate.of(1999, 1, 21)
생성자와 다른 메소드의 중요한 차이점은, 생성자는 반드시 new 연산자와 함께 호출되어야 하며, 이미 존재하는 객체의 필드를 초기화하는 용도로는 사용할 수 없다는 점이다.
A.Employee("B", 250000, 2000, 12, 31) // 컴파일 에러 발생!
4.3.5 Var 변수
다른 언어들처럼 특정 타입을 명시하지 않고 저장할 수 있는 로컬 변수인 var 변수가 Java 10부터 지원된다. var 변수는 초기 주어진 값에 따라 타입이 결정된다.
Employee harry = new Employee("Harry", 50000, 1989, 10, 1);
var harry = new Employee("Harry", 50000, 1989, 10, 1);
이는 타입 이름인 Employee의 반복 사용을 줄이기 위해 사용할 수 있다.
단, Java API의 추가적인 지식 없이 오른쪽에 나타나는 타입만으로 유추할 수 있는 경우에 사용하는 것이 좋으며, int, long, double 등의 숫자 타입에는 가독성을 위해 사용하지 않는 것이 좋다.
또한, var 키워드는 메소드 내부의 로컬 변수에만 사용할 수 있고, 메소드의 파라미터나 필드에는 사용할 수 없다.
4.3.6 null 참조 처리
만약 null 값에 대해 메소드를 실행하면 NullPointerException이 발생한다.
LocalDate rightNow = null;
String s = rightNow.toString(); // NullPointerException 발생
이는 “배열 범위 초과(index out of bounds)”와 같이 매우 심각한 에러로, 예외를 적절히 처리하지 않으면 프로그램이 종료된다. 일반적으로 프로그램은 이러한 종류의 예외를 직접 처리하기보다는, 처음부터 예외가 발생하지 않도록 설계하는 것에 의존한다.
클래스를 설계할 때, 어떤 필드가 null이 될 수 있는지를 명확히 하는 것이 중요하다. 예를 들어, name이나 hireDay 필드가 null이 되는 것을 원하지 않는다면, Objects 클래스에서 제공하는 메소드를 활용할 수 있다.
public Employee(String n, double s, int year, int month, int day) {
name = Objects.requireNonNullElse(n, "unknown");
...
}
또는 보다 강경하게 null 인수를 거부할 수 있다.
public Employee(String n, double s, int year, int month, int day) {
name = Objects.requireNonNull(n, "이름은 null이 될 수 없습니다.");
...
}
null 값으로 Employee 객체가 생성되면 NullPointerException이 발생하며, 이는 예외 리포트에서 문제의 설명과 발생 위치를 명확히 알 수 있게 해준다.
4.3.7 묵시적(Implicit)과 명시적(Explicit) 파라미터
메소드는 객체 내에서 동작하며 인스턴스 필드에 접근한다. 예를 들어,
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
이 메소드가 실행될 때, salary 인스턴스 필드의 값이 변경된다.
number007.raiseSalary(5);
위 코드는 number007 객체의 salary 필드 값을 5% 증가시킨다. 보다 자세히 표현하면,
double raise = number007.salary * 5 / 100;
number007.salary += raise;
여기서 raiseSalary 메소드는 두 개의 파라미터를 사용한다.
• 첫 번째 파라미터는 묵시적 파라미터로, 메소드 이름 앞에 암시적으로 존재하는 Employee 객체(여기서는 number007)이다.
• 두 번째 파라미터는 메소드 선언 시 괄호 안에 명시적으로 선언된 숫자 값이다. 이것이 명시적 파라미터이다.
모든 메소드에서 this 키워드는 묵시적 파라미터를 가리키며, 필요시 다음과 같이 사용할 수 있다.
public void raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
일부 프로그래머들은 인스턴스 필드와 로컬 변수를 명확히 구분하기 위해 이러한 스타일을 선호한다.
4.3.8 캡슐화의 장점
다음은 getName(), getSalary(), getHireDay() 메소드에 대한 예시이다.
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
이들 메소드는 인스턴스 필드의 값을 단순히 반환하며, 필드 접근자(accessor)라고도 한다.
물론, 단순히 필드를 public으로 선언하는 것이 쉬워 보일 수 있으나,
• name 필드는 읽기 전용으로, 생성 시 정해진 이후에는 변경되지 않아야 한다.
• salary 필드는 읽기 전용은 아니지만, 오직 raiseSalary 메소드를 통해서만 변경되어야 한다.
만약 public 필드로 선언된다면, 외부의 다른 코드가 임의로 접근하여 수정할 수 있으므로, 문제 발생 시 원인을 파악하기 어려워진다.
때로는 인스턴스 필드에 대해 getter(접근자)와 setter(변경자) 메소드를 제공하는 것이 필요하다. 즉,
• private 인스턴스 필드
• public 접근자 메소드
• public 변경자 메소드
를 제공하는 방식이다.
이 방식은 단순히 public 인스턴스 필드를 제공하는 것보다 번거롭지만, 클래스 내부 구현의 변경이나 오류 검사를 용이하게 한다.
주의: 접근자 메소드가 변경 가능한 객체를 반환하는 경우 주의가 필요하다.
class Employee {
private Date hireDay;
...
public Date getHireDay() {
return hireDay; // 변경 가능 → 외부에서 hireDay를 변경할 수 있음 (BAD)
}
...
}
Date 클래스는 setTime 등의 변경자 메소드를 가지고 있으므로, 이를 그대로 반환하면 외부에서 객체의 상태를 변경할 수 있게 되어 캡슐화가 깨진다.
Employee harry = ...;
Date d = harry.getHireDay();
double tenYearsInMilliseconds = 10 * 365.25 * 24 * 60 * 60 * 1000;
d.setTime(d.getTime() - (long) tenYearsInMilliseconds);
위 코드에서는 harry.hireDay와 d가 동일한 객체를 참조하게 되어, d의 상태 변화가 harry.hireDay에도 영향을 준다.

변경 가능한 객체를 반환할 때는, 새로운 객체 복사본을 반환하도록 clone 메소드를 사용하는 것이 좋다.
class Employee {
...
public Date getHireDay() {
return (Date) hireDay.clone();
}
...
}
4.3.9 클래스 기반 접근 권한
메소드가 실행될 때, 해당 메소드는 자신이 속한 클래스의 private 데이터에 접근할 수 있다. 일부 사람들은 같은 클래스에서 생성된 모든 객체들의 private 데이터에 접근할 수 있다는 사실에 놀란다.
예를 들어, 두 개의 Employee 객체를 다음과 같이 비교할 때,
class Employee {
...
public boolean equals(Employee other) {
return name.equals(other.name);
}
}
if (harry.equals(boss)) ...
이 메소드는 당연히 harry의 private 필드에 접근하지만, 동시에 boss의 private 필드에도 접근한다. 이는 두 객체 모두 Employee 타입이기 때문에 가능하며, Employee 클래스의 메소드는 어떠한 Employee 객체에 대해서도 private 필드에 접근할 수 있다.
4.3.10 Private 메소드
클래스를 구현할 때, 인스턴스 필드와 같이 외부에 노출되는 public 데이터는 위험하므로 모두 private으로 지정하는 것이 좋다. 그러나 메소드의 경우 대부분은 public이지만, 특정 상황에서만 사용되는 도우미(helper) 메소드나 내부 계산 코드는 public 인터페이스에 포함될 필요가 없다. 이러한 메소드는 클래스의 내부 구현과 밀접하게 연관되어 있고, 특정 호출 순서나 프로토콜을 필요로 할 수 있으므로 private으로 구현하는 것이 최선이다.
메소드를 private으로 선언하면, 구현하는 과정에서 해당 메소드를 외부에서 사용할 필요가 없어지며, 나중에 내부 데이터 표현 방식이 변경되더라도 영향을 받지 않고 쉽게 수정하거나 제거할 수 있다. 만약 메소드가 public이면, 다른 코드에서 이를 사용하게 되어 쉽게 제거하기 어려워진다.
// 할인액 계산 (private 메소드)
private double disc(double price, double rate) {
return price * rate / 100;
}
// 최종 가격 계산 (public 메소드)
public double finalPrice(double price, double rate) {
return price - disc(price, rate);
}
4.3.11 Final 인스턴스 필드
인스턴스 필드를 final로 지정할 수 있다. final 필드는 객체가 생성될 때 반드시 초기화되어야 하며, 모든 생성자의 종료 시점에는 값이 할당되어 있어야 한다. 한 번 설정되면 나중에 값을 변경할 수 없다.
예를 들어, Employee 클래스의 name 필드는 객체 생성 후 변경되지 않으므로 final로 선언된다.
class Employee {
private final String name;
...
}
final 한정자는 기본 타입이나 불변 클래스의 필드에 특히 유용하다. (객체의 상태가 변경되지 않으면 해당 클래스는 불변하며, String 클래스가 그 대표적인 예이다.)
반면, 변경 가능한 클래스의 경우 final 한정자는 변수에 저장된 객체 참조가 다른 객체를 참조하지 않도록 보장할 뿐, 객체 내부의 상태는 변경할 수 있다.
예를 들어,
private final StringBuilder evaluations;
는 Employee의 생성자에서 다음과 같이 초기화된다.
evaluations = new StringBuilder();
여기서 final 키워드는 evaluations 변수가 다른 StringBuilder 객체를 참조하지 않음을 보장한다. 당연히 StringBuilder 객체 자체는 변경 가능하다.
public void giveGoldStar() {
evaluations.append(LocalDate.now() + " : Gold Star\n");
}
2025.03.08 - [Core Java/Chapter 4] - 코어자바(Core Java) 12판 Chapter 4 리뷰 : 4.4 정적 필드와 메소드
코어자바(Core Java) 12판 Chapter 4 리뷰 : 4.4 정적 필드와 메소드
2025.03.08 - [Core Java/Chapter 4] - 코어자바(Core Java) 12판 Chapter 4 리뷰 : 4.3 자신만의 클래스 정의하기 코어자바(Core Java) 12판 Chapter 4 리뷰 : 4.3 자신만의 클래스 정의하기2025.03.08 - [Core Java/Chapter 4] - 코
choosla.tistory.com
'Core Java > Chapter 4' 카테고리의 다른 글
| 코어자바(Core Java) 12판 Chapter 4 리뷰 : 4.6 객체 생성 (0) | 2025.03.08 |
|---|---|
| 코어자바(Core Java) 12판 Chapter 4 리뷰 : 4.5 메소드 매개변수 (0) | 2025.03.08 |
| 코어자바(Core Java) 12판 Chapter 4 리뷰 : 4.4 정적 필드와 메소드 (0) | 2025.03.08 |
| 코어자바(Core Java) 12판 Chapter 4 리뷰 : 4.2 표준 라이브러리 클래스 사용하기 (0) | 2025.03.08 |
| 코어자바(Core Java) 12판 Chapter 4 리뷰 : 4.1 객체(Object)와 객체 지향 프로그래밍 (0) | 2025.03.08 |