자바 클래스(Class) 제대로 이해하기 — 구조·생성자·정적 멤버·내부 클래스
개념 요약
- 클래스(Class): 데이터(필드)와 동작(메서드)을 한 덩어리로 묶는 설계도. 객체(Object)는 클래스의 인스턴스.
- 캡슐화(Encapsulation): 필드를
private으로 숨기고 메서드로만 상태 변경. 불변(Immutable) 여부도 설계로 결정. - 생성자(Constructor): 인스턴스 초기화 진입점. 오버로딩 가능,
this(...)로 다른 생성자 호출. - 정적 멤버(Static member): 인스턴스 없이 클래스 차원에서 공유되는 데이터/동작.
- 접근 제어자(Access Modifier): 가시성을 제한해 결합도를 낮춤(
public/protected/패키지 전용/private). - 내부/중첩 클래스(Inner/Nested class): 클래스 내부에 선언된 클래스.
static여부에 따라 바깥 인스턴스 의존성이 달라짐.
왜 필요한가?
- 복잡한 로직을 역할 단위로 나눠 재사용·테스트·유지보수를 쉽게 만든다.
- 불변성과 가시성 제어로 버그를 줄이고 API 계약을 명확히 한다.
- 정적 멤버로 공용 상수/유틸 메서드를 제공하거나, 빌더 같은 패턴을 깔끔하게 구현한다.
접근 제어자 요약
| 제어자 | 어디서 보이나 | 주용도 |
|---|---|---|
public |
어디서나 | 외부 API 공개 |
protected |
같은 패키지 + 하위 클래스 | 상속 확장 지점 |
| (없음, package-private) | 같은 패키지 | 모듈 내부 캡슐화 |
private |
같은 클래스 | 구현 세부 숨김 |
예제 코드
예제 1) 필드/생성자/정적 멤버/캡슐화
// 파일: User.java
public class User {
private final long id; // 불변 필드
private String name; // 가변 필드 (필요 시 메서드로만 변경)
private static int instanceCount = 0; // 클래스 전체에서 공유
public User(long id, String name) {
if (name == null || name.isBlank()) throw new IllegalArgumentException("name");
this.id = id;
this.name = name;
instanceCount++;
}
public long getId() { return id; }
public String getName() { return name; }
public void rename(String newName) {
if (newName == null || newName.isBlank()) throw new IllegalArgumentException("newName");
this.name = newName;
}
public static int getInstanceCount() { return instanceCount; }
@Override public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
}
// 사용 예시
public static void main(String[] args) {
User u = new User(1L, "Kim");
u.rename("Lee");
System.out.println(u); // User{id=1, name='Lee'}
System.out.println(User.getInstanceCount()); // 1
}
}
예제 2) 정적 중첩 클래스(Builder) vs 내부 클래스(Outer 인스턴스 캡처)
// 파일: Product.java
public class Product {
private final String name;
private final int price;
private Product(Builder b) { // 빌더에서 값 복사
this.name = b.name;
this.price = b.price;
}
// 정적 중첩 클래스: 바깥 Product 인스턴스에 의존하지 않음
public static class Builder {
private String name;
private int price;
public Builder name(String n) { this.name = n; return this; }
public Builder price(int p) { this.price = p; return this; }
public Product build() { return new Product(this); }
}
// 비정적 내부 클래스: 바깥 Product 인스턴스에 묶임
public class Review {
private final String text;
public Review(String text) { this.text = text; }
public String summary() { return name + " — " + text; } // 바깥 필드 접근 가능
}
// 사용 예시
public static void main(String[] args) {
Product p = new Product.Builder().name("Mouse").price(30000).build();
Product.Review r = p.new Review("가성비 최고");
System.out.println(r.summary()); // Mouse — 가성비 최고
}
}
동작 원리
- 예제 1에서
instanceCount는 클래스 로딩 시 0으로 초기화되고, 생성자가 호출될 때마다 증가한다.User.getInstanceCount()는 인스턴스 없이 호출 가능(정적 메서드). id는final이라 생성자에서만 초기화되고 이후 변경 불가. 반면name은rename()을 통해서만 바뀌게 해 불변·가변 경계를 분명히 했다.toString()오버라이드는 디버깅/로깅 가독성 향상. 필요하면equals()/hashCode()도 값 동등성을 기준으로 재정의한다.- 예제 2에서
Product.Builder는 정적 중첩 클래스라서 바깥 인스턴스 없이 독립적으로 생성 가능(new Product.Builder()). 반면Review는 내부 클래스라p.new Review(...)처럼 바깥 인스턴스를 통해서만 만들 수 있고, 자연스럽게Product의 상태(name)에 접근한다. - 선택 기준: 바깥 상태가 필요 없다면
static중첩 클래스, 바깥 상태를 참조해야 한다면 내부 클래스. 빌더·유틸리티는 보통static이 적합.
추가 팁
- 초기화 순서: 정적 필드 → 정적 초기화 블록 → 인스턴스 필드 → 인스턴스 초기화 블록 → 생성자.
- 유틸 클래스는 인스턴스화 방지를 위해
private생성자 사용. - 가시성 기본값은 기획 단계에서 가장 좁게 시작(예:
private/패키지 전용)하고, 필요할 때만 공개 범위를 넓혀라. - 자바 16+의 record는 값 객체를 간결하게 만들 때 유용하지만, 불변이며 상속 불가라는 제약을 기억하자.
요약
- 클래스는 상태+행동의 설계도이고, 캡슐화로 내부 구현을 숨겨 안정적인 API를 만든다.
- 생성자와 초기화 규칙을 통해 일관된 인스턴스 상태를 보장하고, 정적 멤버로 공용 기능/데이터를 제공한다.
- 중첩 클래스는
static여부로 의존성이 갈린다: 빌더/상수 집합은 정적, 바깥 상태를 캡처해야 하면 내부 클래스를 써라. - 작은 규칙(불변 필드, 검증, 가시성 최소화)을 지키면 테스트성과 유지보수가 크게 좋아진다.
- 이어서 보면 좋은 주제: 상속/다형성(Polymorphism), record/enum과 데이터 모델링.
'Backend > Java' 카테고리의 다른 글
| [JAVA] 예외처리 (0) | 2025.09.22 |
|---|---|
| [JAVA] 반복문 (0) | 2025.09.08 |
| [JAVA] 변수 (0) | 2025.09.08 |
| [Java] 기본형 vs 참조형 (1) | 2025.02.11 |
| 스파르타 코딩 클럽 SQL 1주차 (0) | 2022.06.28 |