1. Getter 허용 기준 (상대적으로 자유로움)
● 허용: 조회 목적의 필드
- 조회용으로 필드 값을 외부에 제공할 필요가 있을 때 허용됩니다.
public String getEmail() {
return email;
}
● 제한 또는 금지:
- 민감 정보 (password, token, salt, secretKey, 등)
- 내부적으로만 사용하는 정보 (internalFlag, deleted, 등)
// ❌ 이렇게 민감한 값은 외부 노출 X
public String getPassword() {
return password;
}
권장:
- 필요한 필드만 Getter 생성 (@Getter 대신 개별 작성)
- Lombok @Getter는 DTO나 읽기 전용 뷰 모델에만 사용
2. Setter 허용 기준 (매우 신중)
● 기본 원칙: 엔터티에 Setter를 직접 노출하지 않는다
- 데이터 변경은 Setter가 아닌 비즈니스 메서드를 통해 이루어져야 합니다.
// ❌ setter 공개 X
public void setRole(String role) {
this.role = role;
}
// ✅ 의미 있는 이름으로 비즈니스 행위로 감쌈
public void changeRole(UserRole newRole) {
this.role = newRole;
}
● 제한적으로 허용하는 경우:
- ID, 생성일 같은 변경 불가능한 필드는 setter 자체를 만들지 않음
- JPA가 내부적으로 사용하는 용도의 필드 (예: 양방향 관계 매핑 시 setParent() 등)는 protected로 제한
3. 실무에서의 Setter 관리 전략
| 조회만 필요한 필드 | public getter 허용 |
| 상태 변경 (예: 이름 변경, 탈퇴 등) | changeName(), deactivate() 처럼 명시적 비즈니스 메서드 사용 |
| JPA 내부 용도 (양방향 관계 연결) | protected setter, 혹은 패키지 접근 수준 제한 |
| Lombok 사용 | @Getter만 선택적 사용. @Setter는 지양 또는 DTO 전용 클래스에서만 사용 |
4. 실전 예시 (추천 구조)
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String email;
private String password;
private String role;
protected User() {} // JPA 기본 생성자
// 생성자 or 팩토리 메서드
private User(String email, String password) {
this.email = email;
this.password = encrypt(password);
this.role = "USER";
}
public static User create(String email, String password) {
return new User(email, password);
}
// getter는 필요한 필드만 공개
public String getEmail() {
return email;
}
public String getRole() {
return role;
}
// setter 대신 비즈니스 메서드 사용
public void changePassword(String rawPassword) {
this.password = encrypt(rawPassword);
}
public void changeRole(String newRole) {
if (!List.of("USER", "ADMIN").contains(newRole)) {
throw new IllegalArgumentException("Invalid role");
}
this.role = newRole;
}
private String encrypt(String raw) {
// 실제 암호화 로직
return raw + "_encrypted";
}
}
1. ⚠ Getter와 Setter의 문제점
1.1 Setter의 위험성
@Entity
public class User {
@Id
private Long id;
private String role;
public void setRole(String role) {
this.role = role;
}
}
위처럼 setter를 공개하면:
- 누구나 객체의 상태를 마음대로 변경할 수 있어요.
- 예: user.setRole("ADMIN") → 관리자 권한 탈취 가능성
- 객체 내부의 비즈니스 규칙을 우회해서 값을 바꿀 수 있어요.
- 예: 비밀번호 암호화 없이 바로 바꾸거나, 유효하지 않은 이메일을 그대로 넣는 경우
- 변경 추적이 어려워짐 → 추후 디버깅, 감사 로직에도 치명적
➡ 문제 요약: 객체의 캡슐화(encapsulation) 원칙을 깨뜨립니다.
1.2 Getter의 위험성
Getter는 Setter보다는 덜 위험하지만, 다음과 같은 문제가 있어요:
- 민감한 데이터(예: password, security token 등)가 노출될 수 있음
- 불필요하게 내부 구현이 외부에 공개됨 → 객체의 내부 정보 유출
- 클라이언트 코드가 getter에 의존해 객체의 상태를 바꾸려는 유혹을 가짐
2. 안전한 대안들
2.1 생성자 + 정적 팩토리 메서드 사용
@Entity
public class User {
@Id
private Long id;
private String email;
private String password;
protected User() {} // JPA용 기본 생성자 (protected)
private User(String email, String password) {
this.email = email;
this.password = password;
}
public static User create(String email, String password) {
// 검증, 암호화 등 비즈니스 로직 추가
return new User(email, encrypt(password));
}
private static String encrypt(String raw) {
return "..."; // 암호화 로직
}
}
➡ 장점: 객체 생성을 제어하고, 생성 시 검증, 로직 삽입 가능
2.2 불변(immutable) 엔터티는 어렵지만, Setter를 제한적으로만 허용
JPA는 리플렉션을 통해 필드를 조작하기 때문에 완전 불변 객체는 어려워요. 따라서 다음 방식으로 제한적으로 Setter를 사용합니다:
public void changePassword(String rawPassword) {
this.password = encrypt(rawPassword);
}
➡ 비즈니스 메서드 형태로 Setter를 감싸기
➡ 필드 이름 그대로 노출하지 않고, 의미 있는 동작으로 감쌈
2.3 Builder 패턴 사용 시 주의점
Builder는 가독성과 객체 생성 유연성에 좋지만, 불완전한 객체 생성 가능성이 있어요.
User.builder()
.email("admin@example.com")
.build(); // password 빠짐 (위험)
➡ 해결책:
- 필수값을 Builder 생성자에 넣거나
- 생성 이후 검증 로직 추가
2.4 Query Projection DTO를 따로 만들기
엔터티에는 비즈니스 로직만 넣고, 조회 용도는 따로 DTO를 사용:
public record UserDto(String email, String role) {}
3. 정리 요약
| Setter | 무분별한 변경 가능성 | 비즈니스 메서드 (changePassword()) |
| Getter | 민감 데이터 노출 | 필요한 필드만 제한 공개 |
| Builder | 불완전 객체 생성 | 생성자 강제 / 검증 메서드 |
| 생성자 | 외부에서 직접 생성 | protected + static factory method |
| 데이터 조회 | 엔터티 노출 위험 | DTO 분리 |
4. 실무에서 추천하는 구조
- 엔터티는 최대한 캡슐화
- 필드 private, Setter 지양
- 생성자 + 팩토리 메서드로 생성
- 변경은 메서드를 통해
- DTO는 별도로 분리
- 조회용, 전송용, 수정용 DTO 각각 사용
- Builder는 DTO에만 쓰는 것이 더 안전
1. @Access(AccessType.FIELD) – JPA의 접근 전략 지정
● 기본 동작:
JPA는 기본적으로 필드 접근 또는 프로퍼티 접근 중 하나를 선택해서 매핑합니다:
- 프로퍼티 접근: getter/setter를 통해 접근 (@Id가 메서드 위에 있으면)
- 필드 접근: private 필드에 직접 접근 (@Id가 필드 위에 있으면)
@Access(AccessType.FIELD)
@Entity
public class User {
@Id
private Long id; // 필드 기준으로 접근
}
● 왜 필요한가?
- Setter 없이도 JPA가 값을 주입할 수 있게 함
→ @Access(AccessType.FIELD)를 설정하면 JPA가 필드에 직접 접근하므로 setter 없이도 동작 - 비즈니스 메서드 외에는 상태 변경을 막을 수 있음
→ 외부에서 getter/setter로 상태를 바꾸지 못하게 제한 가능 - 명확하게 전략을 고정하여 실수 방지
→ 필드와 메서드가 혼합된 경우, 예상과 다른 동작 방지
2. @NoArgsConstructor(access = AccessLevel.PROTECTED) – JPA 기본 생성자 보호
JPA는 내부적으로 프록시를 생성하거나 리플렉션으로 객체를 생성해야 하므로, 반드시 매개변수가 없는 생성자가 필요합니다. 하지만 이 생성자를 public으로 열어두는 건 위험할 수 있습니다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class User {
// 기본 생성자를 외부에서 호출 못하게 보호
}
● 왜 필요한가?
- public 생성자를 두면 외부에서 무분별하게 객체를 생성할 수 있음
- protected로 제한하면 JPA는 생성 가능하고, 개발자는 외부에서 생성 불가
➡ 도메인 객체는 정적 팩토리 메서드로만 생성하게 유도하는 구조로 만들 수 있습니다.
실무 추천 설정 (정리)
import jakarta.persistence.*;
import lombok.*;
@Access(AccessType.FIELD)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String email;
private String password;
private String role;
// 정적 팩토리 메서드
public static User create(String email, String password) {
User user = new User();
user.email = email;
user.password = encrypt(password);
user.role = "USER";
return user;
}
public void changePassword(String rawPassword) {
this.password = encrypt(rawPassword);
}
private static String encrypt(String raw) {
return "..."; // 비밀번호 암호화
}
}
- @Builder를 함께 사용하면 생성자 제한이 무력화될 수 있음 → DTO나 응답 객체에만 사용하세요.
- 도메인 로직이 많은 경우, 엔티티는 가능한 한 읽기 전용(immutable에 가깝게) 설계하세요.
- Spring Data JPA 사용 시에도 위 설정은 매우 권장됩니다.
| @Access(AccessType.FIELD) | JPA가 필드에 직접 접근하도록 설정 | 엔터티 클래스 상단 |
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | 기본 생성자 생성 + 외부 생성 제한 | 엔터티 클래스 상단 |
'Java > Spring Boot' 카테고리의 다른 글
| ResponseEntity (0) | 2025.05.15 |
|---|---|
| Swagger와 API 명세 (1) | 2025.03.31 |
| Spring Boot Test 관련 (JUnit, Mockito, AssertJ,Spring Boot Test, Testcontainers ) (0) | 2025.03.31 |
| Optional<T> (0) | 2025.03.31 |
| JPQL과 QueryDSL (0) | 2025.03.31 |