Skip to content

TIL -2025-06-19 [Setter를 피해야 하는 경우, Setter 대신 사용해야 하는 패턴들, Setter 사용이 허용되는 경우] #83

@soheeGit

Description

@soheeGit

🚫 Setter를 피해야 하는 경우

1. 무분별한 Public Setter (가장 큰 문제)

// ❌ 나쁜 예시 - 무분별한 Setter
@Entity
public class Schedule {
    private String title;
    private LocalDateTime startDate;
    private LocalDateTime endDate;
    
    // ❌ 모든 필드에 대한 무차별 setter
    public void setTitle(String title) { this.title = title; }
    public void setStartDate(LocalDateTime startDate) { this.startDate = startDate; }
    public void setEndDate(LocalDateTime endDate) { this.endDate = endDate; }
}

// 🚨 문제점: 비즈니스 규칙 무시
schedule.setStartDate(startDate);
schedule.setEndDate(endDate);  // 시작일보다 이른 종료일 설정 가능!

2. 객체 불변성 파괴

// ❌ 불변 객체가 되어야 하는 경우
public class Money {
    private BigDecimal amount;
    
    // ❌ Money는 불변이어야 하는데 setter가 있으면 문제
    public void setAmount(BigDecimal amount) {
        this.amount = amount;  // 돈 객체가 변경됨!
    }
}

3. 데이터 일관성 파괴

// ❌ 연관된 필드들을 따로 설정
schedule.setStartDate(newStartDate);
// 다른 코드가 실행되다가...
schedule.setEndDate(newEndDate);  // 중간에 불일치 상태 발생 가능

✅ Setter 대신 사용해야 하는 패턴들

1. 생성자 + Builder (객체 생성 시)

// ✅ 좋은 예시 - Builder 패턴
@Entity
public class Schedule {
    private String title;
    private LocalDateTime startDate;
    private LocalDateTime endDate;
    
    @Builder
    public Schedule(String title, LocalDateTime startDate, LocalDateTime endDate) {
        validateDateRange(startDate, endDate);  // 생성 시 검증
        this.title = title;
        this.startDate = startDate;
        this.endDate = endDate;
    }
    
    private void validateDateRange(LocalDateTime start, LocalDateTime end) {
        if (start.isAfter(end)) {
            throw new IllegalArgumentException("시작일이 종료일보다 늦을 수 없습니다.");
        }
    }
}

2. 의미 있는 메서드 (비즈니스 로직 포함)

// ✅ 좋은 예시 - 비즈니스 의미가 있는 메서드
@Entity
public class Schedule {
    // ❌ public void setTitle(String title)
    // ✅ 의미 있는 메서드명
    public void updateTitle(String newTitle) {
        validateTitle(newTitle);
        this.title = newTitle;
    }
    
    // ❌ public void setStartDate(), setEndDate() 따로
    // ✅ 연관된 데이터를 함께 처리
    public void updateDateTime(LocalDateTime newStartDate, LocalDateTime newEndDate) {
        validateDateRange(newStartDate, newEndDate);
        this.startDate = newStartDate;
        this.endDate = newEndDate;
    }
    
    // ✅ 비즈니스 행위를 나타내는 메서드
    public void postpone(Duration duration) {
        this.startDate = this.startDate.plus(duration);
        this.endDate = this.endDate.plus(duration);
    }
    
    private void validateTitle(String title) {
        if (title == null || title.trim().isEmpty()) {
            throw new IllegalArgumentException("제목은 필수입니다.");
        }
    }
}

🎯 Setter 사용이 허용되는 경우

1. JPA 엔티티의 Package-Private Setter

// ✅ JPA/Hibernate를 위한 제한적 setter
@Entity
public class Schedule {
    private String title;
    
    // JPA 프록시 생성을 위한 기본 생성자
    protected Schedule() {}
    
    // ✅ JPA를 위한 package-private setter (외부 접근 불가)
    void setTitle(String title) {  // package-private
        this.title = title;
    }
    
    // ✅ 비즈니스 로직이 있는 public 메서드
    public void updateTitle(String newTitle) {
        validateTitle(newTitle);
        setTitle(newTitle);  // 내부에서만 호출
    }
}

2. DTO/Request 객체의 Setter

// ✅ 데이터 전송용 객체는 setter 허용
@Getter @Setter  // Lombok으로 생성
public class UpdateScheduleRequest {
    private String title;
    private LocalDateTime startDate;
    private LocalDateTime endDate;
    
    // DTO는 단순 데이터 컨테이너이므로 setter 허용
    // 비즈니스 로직은 Service나 Entity에서 처리
}

3. 테스트에서의 임시 Setter

// ✅ 테스트에서만 사용하는 setter
@Entity
public class Schedule {
    // ... 비즈니스 메서드들
    
    // ✅ 테스트 전용 (패키지 접근 제한)
    @VisibleForTesting
    void setCreatedAtForTest(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }
}

🎯 결론 및 가이드라인

✅ 좋은 패턴

  1. 생성 시: Builder 패턴 사용
  2. 수정 시: 의미 있는 비즈니스 메서드 사용
  3. DTO: Lombok @Setter 허용
  4. 검증: 메서드 내부에서 비즈니스 규칙 검증

❌ 피해야 할 패턴

  1. 무분별한 public setter: 모든 필드에 setter
  2. 비즈니스 로직 없는 setter: 단순 할당만 하는 setter
  3. 불변 객체의 setter: 값 객체의 변경 허용
  4. 도메인 모델의 setter: 엔티티의 일관성 파괴

📝 실용적인 규칙

// ✅ 이렇게 사용하세요
Schedule schedule = Schedule.builder()     // 생성 시
        .title("새 일정")
        .startDate(startDate)
        .build();

schedule.updateTitle("수정된 제목");        // 수정 시
schedule.postpone(Duration.ofHours(1));   // 비즈니스 행위
schedule.makeAllDay();                    // 의미 있는 변경

핵심: Setter를 무조건 금지하는 것이 아니라, 의미 있고 안전한 방식으로 객체를 변경하는 것이 목표! 🎯

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions