🔒 private vs private final - 단 하나의 키워드가 만드는 차이
“final 하나 붙이면 뭐가 달라지나요?” 🤔
이 질문은 자바 객체 설계의 핵심입니다. 실무에서 정말 중요한 개념이니 확실히 잡고 가세요!
🎯 한눈에 비교하기
| 구분 | private |
private final |
|---|---|---|
| 💫 값 변경 | ⭕ 가능 | ❌ 불가능 |
| ⏰ 초기화 시점 | 언제든 가능 | 생성 시 단 1번 |
| 🔄 재할당 | ⭕ 가능 | ❌ 불가능 |
| 🛡️ 객체 불변성 | 낮음 | 높음 |
| 🎨 설계 의도 | 가변 객체 | 불변 객체 |
📝 private 필드 - “변할 수 있는 상태”
특징
private int level; // 얼마든지 바꿀 수 있어요!
- ✅ 클래스 내부에서 자유롭게 변경 가능
- ✅ setter로 값 변경 가능
- ✅ 상태가 계속 바뀌는 객체에 사용
실전 예시
public class Monster {
private int level; // 레벨은 계속 올라가죠
private int hp; // HP는 변동됩니다
private int exp; // 경험치도 계속 쌓입니다
public void levelUp() {
this.level++; // ✅ 변경 가능!
this.hp = level * 100;
}
public void takeDamage(int damage) {
this.hp -= damage; // ✅ 변경 가능!
}
}
🎮 언제 사용?
- 게임 캐릭터의 레벨, HP, 경험치
- 주문의 상태 (대기 → 진행 → 완료)
- 카운터, 점수, 진행률
- “시간에 따라 변하는 것들”
🔐 private final 필드 - “절대 변하지 않는 값”
특징
private final String name; // 한 번 정하면 끝!
- ✅ 딱 한 번만 초기화
- ✅ 이후 절대 변경 불가
- ✅ 생성자에서 반드시 값 할당
실전 예시
public class Monster {
private final String name; // 이름은 바뀌면 안 됨
private final String species; // 종족도 바뀌면 안 됨
private final LocalDate birthDate; // 생일도 바뀌면 안 됨
public Monster(String name, String species) {
this.name = name; // ✅ 생성 시 한 번만!
this.species = species;
this.birthDate = LocalDate.now();
}
// ❌ 이런 메서드는 만들 수 없어요
// public void changeName(String newName) {
// this.name = newName; // 컴파일 에러!
// }
}
⚠️ 컴파일 에러 발생 케이스
Monster pikachu = new Monster("피카츄", "전기");
pikachu.name = "라이츄"; // ❌ 컴파일 에러!
💎 왜 final이 이렇게 중요할까? (실무 관점)
1️⃣ 객체 무결성 보장 🛡️
public class Order {
private final Long orderId; // 주문 ID는 절대 바뀌면 안 됨
private final Long memberId; // 주문한 사람도 바뀌면 안 됨
private final LocalDateTime orderDate; // 주문 날짜도 바뀌면 안 됨
private OrderStatus status; // 상태는 변할 수 있음 (대기→완료)
}
🤔 만약 orderId가 중간에 바뀐다면?
- 주문 추적 불가능
- 결제 정보와 불일치
- 데이터 정합성 깨짐
- 치명적인 버그 발생! 💥
👉 final로 “이건 절대 변하면 안 돼!” 를 명시
2️⃣ 버그 예방 - 의도치 않은 변경 차단 🐛
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// 실수로도 이런 코드를 못 씀
// this.userRepository = null; ❌ 컴파일 에러!
}
실제 사례:
// final 없으면 이런 실수 가능
public void processOrder(Order order) {
order = new Order(); // ⚠️ 파라미터를 실수로 재할당!
// 원래 order는 사라짐...
}
// final 있으면 컴파일 에러
public void processOrder(final Order order) {
order = new Order(); // ❌ 컴파일 에러! 미연에 방지
}
3️⃣ 스레드 안전성 향상 🔒
public class Configuration {
private final String apiKey; // ✅ 멀티스레드 안전
private final int maxConnections; // ✅ 동시 접근해도 안전
// final 필드는 생성 이후 모든 스레드에서 안전하게 읽을 수 있음
}
왜 안전한가?
- 생성 후 값이 절대 안 바뀜
- 동기화 필요 없음
- 멀티스레드 환경에서 안정적
4️⃣ 코드 가독성 - 설계 의도가 명확 📖
@Entity
public class Member {
private final Long id; // 🔑 정체성 (Identity)
private final String email; // 🔑 불변 식별자
private final LocalDateTime joinDate; // 🔑 변하지 않는 사실
private String nickname; // 📝 변경 가능한 정보
private String profileImage; // 📝 변경 가능한 정보
private int point; // 📝 계속 변하는 상태
}
👀 한눈에 알 수 있음:
-
final= 핵심 불변 속성 -
private= 변경 가능한 상태
🗄️ JPA Entity에서의 현실
⚠️ JPA에서 final 사용 시 주의사항
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue
private Long id; // ❌ final 쓰지 않음!
private String name;
private String email;
private final LocalDateTime createdAt = LocalDateTime.now(); // ⭕ 가능
}
🤔 왜 @Id는 final을 안 쓰나요?
1. JPA는 리플렉션으로 필드 값 설정
2. @GeneratedValue는 객체 생성 후 DB가 값 할당
3. final이면 생성 후 값 변경 불가
4. 충돌 발생! 💥
📌 실무 규칙:
| 케이스 | final 사용 |
|---|---|
@Id @GeneratedValue |
❌ 사용 불가 |
| 생성 시점 확정 값 | ⭕ 사용 가능 |
| 비즈니스 키 (이메일 등) | ⚠️ 신중하게 |
| 변경 가능한 필드 | ❌ 사용 안 함 |
📦 DTO / VO에서는 적극 활용!
✨ 완전 불변 DTO
public class MemberResponseDto {
private final Long id;
private final String name;
private final String email;
private final LocalDateTime joinDate;
@Builder
public MemberResponseDto(Long id, String name, String email, LocalDateTime joinDate) {
this.id = id;
this.name = name;
this.email = email;
this.joinDate = joinDate;
}
// Getter만 있고 Setter 없음!
}
✅ 장점:
- 완전한 불변 객체
- 멀티스레드 안전
- 버그 위험 제로
- 의도가 명확
💰 VO (Value Object) 예시
public class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// 값을 바꾸는게 아니라 새 객체를 반환
public Money add(Money other) {
return new Money(
this.amount.add(other.amount),
this.currency
);
}
}
🎯 언제 무엇을 쓸까? (판단 기준표)
✅ private 사용해야 할 때
// 상태가 변하는 것들
private int hp; // HP는 줄었다 늘었다
private OrderStatus status; // 주문 상태 변경
private int quantity; // 수량 변동
private boolean isActive; // 활성화 토글
// JPA Entity의 대부분 필드
@Entity
public class Product {
@Id @GeneratedValue
private Long id; // JPA가 세팅
private String name; // 상품명 변경 가능
private int price; // 가격 변동
}
✅ private final 사용해야 할 때
// 객체의 정체성
private final Long id; // 식별자
private final String code; // 고유 코드
// 절대 변하면 안 되는 값
private final String email; // 이메일 (로그인 ID)
private final LocalDate birthDate; // 생일
// 생성 시 확정되는 값
private final LocalDateTime createdAt;
private final String createdBy;
// DTO, VO의 모든 필드
public class UserDto {
private final String name;
private final int age;
}
// 의존성 주입받는 필드
public class MemberService {
private final MemberRepository memberRepository;
private final EmailService emailService;
}
🧠 실무 판단 공식
💡 이 질문에 답하세요
"이 값이 객체 생명주기 중간에 바뀌면 이상한가?"
| 답변 | 사용할 키워드 |
|---|---|
| YES - 바뀌면 이상함 | private final |
| NO - 바뀌어도 됨 | private |
📋 체크리스트
-
이 값은 객체의 정체성인가? →
final -
이 값은 생성 후 절대 안 바뀌나? →
final -
이 값은 설정값인가? →
final -
JPA의 @GeneratedValue인가? →
private -
상태 변화를 추적해야 하나? →
private
🎨 실전 종합 예제
Case 1: 게임 몬스터 클래스
public class Monster {
// 불변 속성 (정체성)
private final String id; // 고유 ID
private final String name; // 이름
private final MonsterType type; // 종족
private final LocalDateTime createdAt;
// 가변 속성 (상태)
private int level; // 레벨 올라감
private int hp; // HP 변동
private int exp; // 경험치 증가
private boolean isAlive; // 생존 여부
@Builder
public Monster(String id, String name, MonsterType type) {
this.id = id;
this.name = name;
this.type = type;
this.createdAt = LocalDateTime.now();
// 가변 필드는 기본값으로 초기화
this.level = 1;
this.hp = 100;
this.exp = 0;
this.isAlive = true;
}
}
Case 2: 주문 도메인
public class Order {
// 불변 속성
private final Long orderId;
private final Long memberId;
private final LocalDateTime orderDate;
private final String orderNumber;
// 가변 속성
private OrderStatus status; // 대기 → 진행 → 완료
private LocalDateTime completedAt; // 완료 시점
private String cancelReason; // 취소 사유
public void complete() {
this.status = OrderStatus.COMPLETED;
this.completedAt = LocalDateTime.now();
}
public void cancel(String reason) {
this.status = OrderStatus.CANCELED;
this.cancelReason = reason;
}
}
🔥 핵심 정리
💬 한 줄 요약
private final은 설계 선언이다.
“이 값은 절대 바뀌면 안 된다”는 개발자의 의지 표현! 💪
📌 기억해야 할 3가지
-
final은 컴파일러의 도움을 받는 것 - 실수를 미연에 방지 - 불변성은 안정성 - 멀티스레드, 버그 예방, 코드 품질
- 설계 의도 명시 - 코드만 봐도 “이건 안 바뀌는구나” 알 수 있음
🎯 실무 팁
// ✅ 좋은 습관
private final MemberRepository memberRepository; // 의존성은 final
private final String API_KEY = "..."; // 상수는 final
private final LocalDateTime createdAt; // 생성 시각은 final
// ⚠️ 주의
@Id @GeneratedValue
private Long id; // JPA는 final 안 됨
// ❌ 피해야 할 패턴
private String name; // 변하면 안 되는데 final 안 붙임
이제 private와 private final의 차이를 완벽하게 이해하셨나요? 🎉
“불변으로 만들 수 있다면 불변으로 만들어라” - 이것이 좋은 객체 설계의 시작입니다!